1
\$\begingroup\$

I'm a hobbyist Python programmer with not much experience.

I wrote some code for a solution to a Tic Tac Toe AI problem on the internet.

Then yesterday I wrote a simple console Tic Tac Toe game for human vs computer play where the computer plays randomly.

So I thought it would be nice how difficult it would be to combine the code from the AI solution with the console game, so the computer plays intelligently rather than randomly.

Final Product:

import random


class TTTGame(object):

    _coords = [(x, y) for y in range(3) for x in range(3)]
    _num_to_coord = dict([(n+1, c) for n, c in enumerate(_coords)])
    _win_ways = (# Horizontal
                 set([1, 2, 3]),
                 set([4, 5, 6]),
                 set([7, 8, 9]),
                 # Verticle
                 set([1, 4, 7]),
                 set([2, 5, 8]),
                 set([3, 6, 9]),
                 # Diagonal
                 set([1, 5, 9]),
                 set([3, 5, 7]))

    def __init__(self, game=None):
        if not game:
            self.board = map(list, ['___'] * 3)
            self.avail = set(xrange(1, 10))
            self.xnums = set()
            self.onums = set()
        else:
            self.board = map(list, game.board)
            self.avail = set(game.avail)
            self.xnums = set(game.xnums)
            self.onums = set(game.onums)

    def __str__(self):
        rows = [' | '.join(row).replace('_',' ')
                for row in self.board[::-1]]
        divs = ['\n---+---+---\n', '\n---+---+---\n', '\n']
        return ' ' + ' '.join(map(''.join, zip(rows, divs)))

    def winner(self):
        """Return 'X' or 'O' or 'T' else False if game not over."""
        for way in self._win_ways:
            if way.issubset(self.xnums):
                return 'X'
            if way.issubset(self.onums):
                return 'O'
        if len(self.xnums) + len(self.onums) == 9:
            return 'T'
        return False

    def play(self, n, piece=None):
        """
        Place piece at spot numbered n and ammend attributes.
        if piece is None, remove the piece at cell n and ammend attributes.
        """
        x, y = self._num_to_coord[n]
        if piece:
            self.board[y][x] = piece
            (self.xnums if piece == 'X' else self.onums).add(n)
            self.avail.remove(n)
        else:
            self.board[y][x] = '_'
            self.avail.add(n)
            (self.xnums if n in self.xnums else self.onums).remove(n)

    def next_move(self, piece):
        """
        Return the next best move for piece as cell number.
        """
        if all(map(lambda x: len(set(x)) == 1, self.board)):
            return random.choice((1, 3, 7, 9)) # Corners are best first play.
        scores = []
        avail = list(self.avail)
        for n in avail:
            node = TTTGame(self)
            node.play(n, piece)
            scores.append(node._evaluate(piece))
        best = max(enumerate(scores), key=lambda x: x[1])[0]
        return avail[best]

    def _evaluate(self, piece):
        """
        Return a score for how favourable the current board is towards piece.
        """
        state = self.winner()
        if state:
            return (1 if state == piece else 0 if state == 'T' else -1)
        scores = []
        apponent = 'OX'.replace(piece, '')
        for n in self.avail:
            self.play(n, apponent)
            scores.append(0-self._evaluate(apponent))
            self.play(n) # reverse play
        safest = min(scores)
        return safest



class CLI(object):
    # - Note that game_loop() is not concerned with any of the display
    #   attributes or refreshing the screen.
    # - Each method is responsible for
    #   handling it's own display characteristics, for now, with the use of
    #   the message attribute and refresh().
    # - Methods which modify the message attribute should assign an empty 
    #   string to message attribute before returning, except in cases like
    #   coin_toss().
    # - Methods may communicate with each other via return values, not
    #   via message or any other display attribute.

    def __init__(self):
        # Display variables.
        self.wins = 0
        self.losses = 0
        self.ties = 0
        self.message = ''
        # Piece assignments.
        self.player = ''
        self.computer = ''
        # TTTGame instance.
        self.game = None
        # Players turn or not.
        self.turn = None

    def refresh(self):
        screen = '\n' * 100 # Clear screen.
        screen += "TicTacToe\n"
        screen += ("wins:%s\tlosses:%s\tties:%s\n\n" %
                   (self.wins, self.losses, self.ties))
        screen += str(self.game) if self.game else '\n' * 4 # The game board.
        screen += '\n' + self.message + '\n'
        print screen

    def coin_toss(self):
        """Assigns player a piece at random, Returns None."""
        while True: # until user enters valid input
            self.refresh()
            option = raw_input("Heads or Tails (or just hit enter)? ")
            if option.lower() in ['', 'heads', 'h', 'tails', 't']:
                self.player = random.choice(['X', 'O'])
                self.computer = 'XO'.replace(self.player, '')
                break
            else:
                self.message = "That's not a valid choice!"
        self.message = "You go first" if self.player == 'X' else ''

    def player_turn(self):
        while True: # until user enters valid input
            self.refresh()
            option = raw_input("cell number: ").strip().lower()
            if option == 'hint':
                self.message = str(self.game.next_move(self.player))
            elif not (option.isdigit() and 1 <= int(option) <= 9):
                self.message = "That's not a valid option!"
            elif int(option) not in self.game.avail:
                self.message = "That cell is already occupied!"
            else:
                break
        self.game.play(int(option), self.player)
        self.message = ''

    def computer_turn(self):
        self.refresh()
        best = self.game.next_move(self.computer)
        self.game.play(best, self.computer)
        self.message = ''

    def play_again(self):
        """Gives user the chance to quit the program or continue."""
        while True: # until user enters valid input
            self.refresh()
            option = raw_input("Play again (enter) or n? ").strip().lower()
            if not option:
                self.message = ''
                return
            elif option in ["no", 'n']:
                import sys
                sys.exit()
            else:
                self.message = "That's not a valid option!"

    def game_loop(self):
        """Simple game loop."""
        while True:
            self.game = TTTGame()
            self.coin_toss()
            self.turn = (self.player == 'X') # X always goes first.
            while not self.game.winner():
                if self.turn:
                    self.player_turn()
                else:
                    self.computer_turn()
                self.turn = not self.turn
            winner = self.game.winner()
            if winner == 'T':
                self.message = "You tied."
                self.ties += 1
            elif winner == self.player:
                self.message = "You won!"
                self.wins += 1
            else:
                self.message = "You lost."
                self.losses += 1
            self.play_again()


if __name__ == '__main__':
    print '\n' * 100
    print ("Welome to TicTacToe\n\n" +
           "Cells are numbered 1 to 9 and correspond directly\n" +
           "with keys on your keyboards numpad.\n\n" +
           "To make a play, type the relevent number and hit enter\n\n" +
           "You can also type hint when it's your turn to play.\n\n" +
           "BTW, the computer is unbeatable. Which means your win\n" +
           "statistic will never show anything other than 0.\n" +
           "Have fun ;)\n\n")
    raw_input(".... (hit enter) ...")
    user_interface = CLI()
    user_interface.game_loop()
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

From PEP 8 :

Comparisons to singletons like None should always be done with is or is not, never the equality operators.

,

Don't compare boolean values to True or False using ==

and

Imports are always put at the top of the file, just after any module comments and docstrings, and before module globals and constants.

\$\endgroup\$
2
  • \$\begingroup\$ I changed if game == None: to if not game: Although the other cases where I'm using == to compare booleans was because I hadn't thought long enough of the right way to work the return values of TTTGame.winner() function. So I ended up having to do things like if CLI.game.winner() == None which means if game not over and if CLI.game.winner() == False which translates to (kinda) if game tied. \$\endgroup\$
    – Russ
    Commented Jun 17, 2013 at 23:05
  • \$\begingroup\$ Yep, I understand your concern. I was about to comment the fact that the return types were somehow awkward (subtle mix of None, booleans and strings) but I couldn't see any better solution. \$\endgroup\$
    – SylvainD
    Commented Jun 17, 2013 at 23:19
1
\$\begingroup\$
  • The CLI class uses static methods and class attributes for no reason. Use normal instance methods and instance attributes -- it gives you more flexibility and you avoid typing @staticmethod all over the place.
  • winner returns 'X' or 'O' if won else False if tie else None. This is unusual as False is normally used as the opposite of True. You could use some other non-empty string to indicate tie.
\$\endgroup\$
3
  • \$\begingroup\$ Thanks for your input, I really appreciate it. You're right about the winner method and the other poster also picked up on some strange code that was a consequence of the odd nature of winner(). Although I was wondering how instance methods would improve the code. I thought about making an instance class for CLI but it just didn't seem fitting. It doesn't seem like an actual object, but I wanted to encapsulate it all of it incase I needed to import it as a module and working in global just feels messy. \$\endgroup\$
    – Russ
    Commented Jun 17, 2013 at 23:14
  • \$\begingroup\$ @Russ Class attributes share a major characteristic of global variables: there can be only one instance of them. Using class instances and instance variables allows you to have multiple game sessions going on simultaneously, eg. if you develop this into a web application. \$\endgroup\$ Commented Jun 18, 2013 at 5:57
  • \$\begingroup\$ Thanks very much. I completely missed that. I'll start changing it to an instance class today. \$\endgroup\$
    – Russ
    Commented Jun 18, 2013 at 6:31

Your Answer

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

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