Welcome, guest | Sign In | My Account | Store | Cart

Just a simple text based Tic Tac Toe game for human vs computer play.

The game and AI reside in the TTTGame class. AI uses brute force search for 'perfect play'.

The CLI class provides a text based console user interface.

Python, 217 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
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()

1 comment

Zach Arrington 7 years, 8 months ago  # | flag

Hello. I was wondering if you could give a longer explanation of the code. Maybe add comments directly to the code about what it exactly does. Or just an explanation in the comments. THANKS!!

Created by Russel Walker on Tue, 18 Jun 2013 (MIT)
Python recipes (4591)
Russel Walker's recipes (1)

Required Modules

Other Information and Tasks