4
\$\begingroup\$

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()
\$\endgroup\$
  • \$\begingroup\$ [![Some Problem is in your code ](i.stack.imgur.com/pJjnQ.png)](http://i.stack.imgur.com/… 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 Sep 15 '16 at 14:02
2
\$\begingroup\$
  • 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. Catching ValueError 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 returns None for tie and False 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 here

    return 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 return None from result when the game is still on, as there is no result then.

\$\endgroup\$
3
\$\begingroup\$

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!

\$\endgroup\$
  • \$\begingroup\$ Thanks for the review! What would you prefer instead of the system call? \$\endgroup\$ – MrHezi Sep 19 '14 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 Sep 20 '14 at 11:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.