Welcome, guest | Sign In | My Account | Store | Cart
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()

Diff to Previous Revision

--- revision 1 2013-06-18 10:34:29
+++ revision 2 2013-06-18 10:52:27
@@ -49,7 +49,7 @@
     def play(self, n, piece=None):
         """
         Place piece at spot numbered n and ammend attributes.
-        if piece == None, remove the piece at cel 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:
@@ -95,6 +95,16 @@
 
 
 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.
@@ -120,8 +130,8 @@
         print screen
 
     def coin_toss(self):
-        """Mock coin toss. Assigns player a piece at random."""
-        while True:
+        """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']:
@@ -130,19 +140,17 @@
                 break
             else:
                 self.message = "That's not a valid choice!"
-        if self.player == 'X':
-            self.message = "You go first"
+        self.message = "You go first" if self.player == 'X' else ''
 
     def player_turn(self):
-        available = self.game.avail
-        while True:
+        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 available:
+            elif int(option) not in self.game.avail:
                 self.message = "That cell is already occupied!"
             else:
                 break
@@ -157,7 +165,7 @@
 
     def play_again(self):
         """Gives user the chance to quit the program or continue."""
-        while True:
+        while True: # until user enters valid input
             self.refresh()
             option = raw_input("Play again (enter) or n? ").strip().lower()
             if not option:
@@ -169,7 +177,7 @@
             else:
                 self.message = "That's not a valid option!"
 
-    def play(self):
+    def game_loop(self):
         """Simple game loop."""
         while True:
             self.game = TTTGame()
@@ -206,4 +214,4 @@
            "Have fun ;)\n\n")
     raw_input(".... (hit enter) ...")
     user_interface = CLI()
-    user_interface.play()
+    user_interface.game_loop()

History