I am looking for a review of my perfect TicTacToe algorithm. It is split up into 3 parts:
- The function
- The
GameState
Class - The game (NOT well designed)
I am mainly looking for tips and comments on the first 2, but I will also take comments/tips on the poorly designed game.
###alphabeta.py###
#Using wikipedia pseudocode without depth
def alphabeta(game_state, alpha=-2, beta=2, our_turn=True):
if game_state.is_gameover():
return game_state.score(), None
if our_turn:
score = -2 #worst non-possible score. A win, tie, or even a loss will change this
for move in game_state.get_possible_moves():
child = game_state.get_next_state(move, True)
temp_max, _ = alphabeta(child, alpha, beta, False)
if temp_max > score:
score = temp_max
best_move = move
alpha = max(alpha, score)
if beta <= alpha:
break
return score, best_move
else:
score = 2 #worst non-possible score. A win, tie, or even a loss will change this
for move in game_state.get_possible_moves():
child = game_state.get_next_state(move, False)
temp_min, _ = alphabeta(child, alpha, beta, True)
if temp_min < score:
score = temp_min
best_move = move
beta = min(beta, score)
if beta <= alpha:
break
return score, best_move
###gamestate.py###
class GameState:
def __init__(self,board,char='X',oppchar='O'):
self.char = char
self.oppchar = oppchar
self.board = board
self.winning_combos = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]]
def is_gameover(self):
'''returns if a game_state has been won or filled up'''
if self.board.count(self.char) + self.board.count(self.oppchar) == 9:
return True
for combo in self.winning_combos:
if self.board[combo[0]] == self.char and self.board[combo[1]] == self.char and self.board[combo[2]] == self.char:
return True
elif self.board[combo[0]] == self.oppchar and self.board[combo[1]] == self.oppchar and self.board[combo[2]] == self.oppchar:
return True
return False
def score(self):
'''returns 1, 0, or -1 corresponding to the score (WIN, TIE, LOSS) from the ai's point of view'''
if self.board.count(self.char) + self.board.count(self.oppchar) == 9:
return 0
for combo in self.winning_combos:
if self.board[combo[0]] == self.char and self.board[combo[1]] == self.char and self.board[combo[2]] == self.char:
return 1
elif self.board[combo[0]] == self.oppchar and self.board[combo[1]] == self.oppchar and self.board[combo[2]] == self.oppchar:
return -1
@staticmethod
def return_state(score):
'''Returns a word for each score'''
if score == 0:
return('TIED')
elif score == 1:
return('WON')
elif score == -1:
return('LOST')
else:
return('ERROR')
def pretty_print(self):
'''prints the board joined by spaces'''
print(' '.join(self.board[:3]))
print(' '.join(self.board[3:6]))
print(' '.join(self.board[6:9]))
def get_possible_moves(self):
'''returns all possible squares to place a character'''
return [index for index, square in enumerate(self.board) if square != self.char and square != self.oppchar]
def get_next_state(self, move, our_turn):
'''returns the gamestate with the move filled in'''
copy = self.board[:]
copy[move] = self.char if our_turn else self.oppchar
return GameState(copy, char=self.char, oppchar=self.oppchar)
###game###
#!/usr/bin/env python3
import random
from time import sleep
import alphabeta as ab
import gamestate as gs
def get_p_move(game):
while True:
raw_move = input('Your move (1-9) > ')
if raw_move.isdigit():
p_move = int(raw_move) - 1
if p_move > -1 and p_move < 9:
if game.board[p_move] == '_':
break
return p_move
def run_game(game, player_goes_first):
if player_goes_first:
while True:
game.pretty_print()
p_move = get_p_move(game)
game = game.get_next_state(p_move, False)
if game.is_gameover(): break
score, ai_move = ab.alphabeta(game)
game = game.get_next_state(ai_move, True)
if game.is_gameover(): break
else:
while True:
score, ai_move = ab.alphabeta(game)
game = game.get_next_state(ai_move, True)
if game.is_gameover(): break
game.pretty_print()
p_move = get_p_move(game)
game = game.get_next_state(p_move, False)
if game.is_gameover(): break
game.pretty_print()
print('The computer ' + game.return_state(score))
def get_symbols():
human_char = input('Pick your symbol > ')
if len(human_char) > 1 or human_char == '' or human_char == ' ' or human_char == '_':
exit()
elif human_char == 'X' or human_char == 'x':
ai_char = 'O'
else:
ai_char = 'X'
return human_char, ai_char
if __name__ == '__main__':
try:
human_char, ai_char = get_symbols()
start_board = ['_'] * 9
player_goes_first = bool(random.randint(0,2))
input('You go first:{}\nenter to continue\n'.format(player_goes_first))
game = gs.GameState(start_board,char=ai_char, oppchar=human_char)
run_game(game, player_goes_first)
except KeyboardInterrupt:
print('\n')
exit()
1 Answer 1
UX
When I first run the code, I see this message:
Pick your symbol >
It would be helpful to know what my choices are, something like:
human_char = input('Pick your symbol [x or o] > ')
After I pick my symbol, I see:
You go first:True
That looks a little odd. I expect to see:
You go first
or
You go second
When prompted with:
Your move (1-9) >
It would be helpful to the user to know that 1 is top left.
Simpler
In the get_symbols
function, this:
elif human_char == 'X' or human_char == 'x':
can be simplified as:
elif human_char == 'x':
if you use lower
on the input
:
human_char = input('Pick your symbol > ').lower()
Naming
The PEP 8 style guide recommends snake_case for function and variable names.
def alphabeta
would be:
def alpha_beta
Documentation
You should add a docstring for the alphabeta
function. Since the name
is vague, you should add a description of what the function does. The
docstring should also describe the input types and the return type.
It would also be helpful to point out that the function is recursive.
You should also add a docstring for the GameState
class to summarize
its purpose.
Layout
Single lines like:
if game.is_gameover(): break
should be split into 2 lines:
if game.is_gameover():
break
The black program can be used to automatically reformat the code.
There is no need for parentheses for return
statements:
return('TIED')
This is simpler and more conventional:
return 'TIED'
DRY
In the GameState
class, this expression is repeated twice:
if self.board.count(self.char) + self.board.count(self.oppchar) == 9:
Consider moving it to a function that returns a boolean value.
Explore related questions
See similar questions with these tags.