I have written a Tic-Tac-Toe game in Python that contains, among others, a player that uses the minimax algorithm. I am not exactly a beginner at Python, but I'm not very experienced with it - so I want to know if my code follows bad practices and style. Any feedback about it is welcome.
# Play tic-tac-toe. The first player will be always X.
# Each tic-tac-toe board is represented by a sequence of three values:
# (set of squares that have an X, set of squares that have a y, board's width)
import random
import os
import string
def TicTacToe(X, O, width=3):
"""Play a tic-tac-toe game between the two given functions. After each
turn, yield the new board.
Each function should get a tic-tac-toe board and a char - 'X' or 'O',
that represents the current turn, and return the number of
square where it wants to put a sign.
width is the board's width and length - it's 3 as default.
X, O -- functions
width -- natural number
"""
board = (set(), set(), width)
turn = 'X'
while result(board) == False:
if turn == 'X':
board[0].add(X(board, turn))
else:
board[1].add(O(board, turn))
yield board
turn = list({'X', 'O'} - set([turn]))[0]
def displayTicTacToe(X, O, width=3):
"""Play a tic-tac-toe game (see TicTacToe's docstring for explanation) and
display the new board to the user when a player plays, and the result of
the game after its end.
X, O - functions
width - natural number"""
for board in TicTacToe(X, O, width):
os.system('cls' if os.name == 'nt' else 'clear') # clearscreen
print str_board(board)
winner = result(board)
if winner in {'X', 'O'}: print winner + ' won!'
elif winner == None: print 'Tie!'
else: raise ValueError("The game didn't end")
def result(board):
"""Return 'X' if X won in the given board, 'O' if O won, None if the game
ended with a tie, False if the game didn't end yet, and raise an exception
if it looks like X and O won both (the board cannot be reached using a
legal game)."""
x_squares, o_squares, width = board
rows = [{width*row+col+1 for col in range(width)} for row in range(width)]
cols = [{width*row+col+1 for row in range(width)} for col in range(width)]
diagonals = [{width*i+i+1 for i in range(width)},
{width*i+width-i for i in range(width)}]
lines = rows + cols + diagonals
x_won = any(line.issubset(x_squares) for line in lines)
o_won = any(line.issubset(o_squares) for line in lines)
if x_won:
if o_won:
raise ValueError("Illegal board")
return 'X'
if o_won:
return 'O'
if x_squares | o_squares == set(range(1, width**2+1)):
# Nobody won, but the board is full
return None # Tie
return False
def str_board(board):
"""Return the board in a string representation, to print it."""
return_str = ''
x_squares, o_squares, width = board
for row in range(width):
for col in range(width):
square = width*row+col+1
return_str += 'X' if square in x_squares else 'O' if square in \
o_squares else ' '
if col != width-1: return_str += ' | '
if row != width-1: return_str += '\n'+'--+-'*(width-1)+'-\n'
return return_str
def human_player(board, turn):
"""Display the board to the user and ask him where does he want to put a
sign. Return the square."""
x_squares, o_squares, width = board
os.system('cls' if os.name == 'nt' else 'clear') # clear screen
print str_board(board)
while True:
try:
square = int(raw_input('Where do you want to add ' + turn + '? '))
assert 0 < square <= width**2 and \
square not in x_squares | o_squares
return square # this will happen if there were no exceptions
except:
print ('You should write an integer between 1 and '+str(width**2)+
', that represents a blank square.')
def minimax_player(board, turn):
"""Return a square where it's worthwhile to play according to the minimax
algorithm."""
return minimax_best_square(board, turn)[0]
def minimax_score_board(board, turn):
"""Return 1, 0 or -1 according to the minimax algorithm -- 1 if the player
that has the given turn has a winning strategy, 0 if he doesn't have a
winning strategy but he has a tie strategy, and -1 if he will lose anyway
(assuming his opponent is playing a perfect game)."""
if result(board) == turn:
return 1
if result(board) == None:
return 0
if result(board) != False:
return -1
return minimax_best_square(board, turn)[1]
def minimax_best_square(board, turn):
"""Choose a square where it's worthwhile to play in the given board and
turn, and return a tuple of the square's number and it's score according
to the minimax algorithm."""
x_squares, o_squares, width = board
max_score = -2
opponent = list({'X', 'O'} - set([turn]))[0]
squares = list(set(range(1, width**2+1)) - (x_squares | o_squares))
random.shuffle(squares)
for square in squares:
# Iterate over the blank squares, to get the best square to play
new_board = (x_squares | set([square] if turn=='X' else []),) + \
(o_squares | set([square] if turn=='O' else []), width)
score = -minimax_score_board(new_board, opponent)
if score == 1: return (square, 1)
if score > max_score:
max_score, max_square = score, square
return (max_square, max_score)
displayTicTacToe(X = minimax_player, O = human_player, width = 3)
raw_input()
-
\$\begingroup\$ [](https://i.sstatic.net/pJjnQ.png) For some scenario it's not picking proper move .. scenario--> place your move so that computer win in that case computer will not place his move on appropriate place to win \$\endgroup\$shivam– shivam2016年09月15日 14:02:35 +00:00Commented Sep 15, 2016 at 14:02
2 Answers 2
- Don't use assertions to validate user input. Assertions can be disabled at runtime.
- Don't catch all exceptions with a bare
except:
clause. It is now impossible to stop your program with Ctrl-C. CatchingValueError
should be enough to deal with input that can't be converted to integer, and you can raise one as well when the input is out of range. - Name your constants. Using
'X'
and'O'
directly means Python won't give you an error message if you mistype'x'
for instance. Speaking of constants,
result
returnsNone
for tie andFalse
for unfinished game. These meanings are not at all obvious. Using a named constant such as TIE would make your code more self-documenting. See how the comment herereturn None # Tie
becomes redundant here
return TIE
None
is commonly used in place of a missing value. It would be quite logical, and clear enough, to returnNone
fromresult
when the game is still on, as there is no result then.
I think your code is very well written. Also very nice structure. I enjoyed reading it.
Just two notes.
One. This seems too much involved, to my taste:
turn = list({'X', 'O'} - set([turn]))[0]
what about:
turn = 'O' if turn=='X' else 'X'
or maybe
turn = {'O': 'X', 'X': 'O'}[turn]
(added: the difficulty to achieve a DRY result is an indication that the variable turn
could better be an integer in (0,1)
which can be used as an index in the "OX" string.)
Two. This:
os.system('cls' if os.name == 'nt' else 'clear') # clearscreen
is a thing I would avoid. Since the program is so clean and self contained, I would keep it as abstract as possible. Maybe in 5 years there will be an OS which could run python happily but which does not have a 'clear' command... what a pity if your program will break for such a triviality!
And if you insist in using a system call, wrap it in a function, don't write it twice!
-
\$\begingroup\$ Thanks for the review! What would you prefer instead of the system call? \$\endgroup\$MrHezi– MrHezi2014年09月19日 18:40:57 +00:00Commented Sep 19, 2014 at 18:40
-
\$\begingroup\$ I simply would not clear the screen... Or look for some alternative solution, maybe as shown here: forum.codecall.net/topic/… \$\endgroup\$Emanuele Paolini– Emanuele Paolini2014年09月20日 11:32:59 +00:00Commented Sep 20, 2014 at 11:32
Explore related questions
See similar questions with these tags.