4
\$\begingroup\$

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()
toolic
15.2k5 gold badges29 silver badges213 bronze badges
asked Dec 13, 2016 at 13:43
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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.

answered May 30 at 18:30
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.