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

井字游戏(Tic-Tac-Toe)

Python, 386 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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# tictactoe.py
# A simple two-player command-line implementation of Tic-Tac-Toe.
#
# Author: Aseem Kishore
#
# No license... free to use, etc.
#
#
# Documentation:
#
# For this implementation, I numbered the rows 1, 2 and 3, and lettered the
# columns A, B and C. Here are two example displays I can show the user:
#
#      A   B   C                            A   B   C 
#                                                     
# 1      |   |                       1      /   /     
#     ---+---+---       or              ---/---/---   
# 2      |   |                     2      /   /       
#     ---+---+---                     ---/---/---     
# 3      |   |                   3      /   /         
#
# Using this representation, I refer to each square as its (row, col) position.
# The row must always be one of the integers 1, 2, or 3, and the col must always
# be one of the strings 'A', 'B' or 'C'.
#
# As for the actual implementation of the board, there are a few choices. One is
# to simply use nine variables, like A1 = ..., A2 = ..., etc. But this isn't
# clean, and it will make the input cumbersome (e.g. a bunch of if-elif cases to
# decide which variable to change, based on what the user entered.). A better
# option is to use a list of 9 elements. The 9 can be numbered in any way, e.g.
# top-bottom, left-to-right, so board[6] would be the equivalent of (2, 'C').
# What I chose to go with was a list of three sub-lists, where each sub-list
# represents a row and contains three elements. So, board[1][2] is the new
# equivalent of (2, 'C').
#
# This implementation is not trivial. I can't easily recognize that board[1][2]
# is the same as the user's (2, 'C'). Plus, my rows are numbered 1-3, and index
# values always begin at 0, so there's the potential for an accidental bug. For
# these reasons, it's always good to use *abstraction* -- hiding the actual
# implementation, and instead using simple values everywhere rather than actual
# implementation values (in this case, board[1][2] is an implementation value).
#
# To abstract this away, I have a few functions which take care of converting
# between conceptual values like square 2C and implementation values like
# board[1][2]:
#
# - square(row, col) takes a row and a col and makes a (row, col) tuple out of
#   them. If I use this function everywhere instead of directly making tuples,
#   I enforce that if I ever want to change the implementation (without having
#   to change the conceptual values like square 2C), I can do so here without
#   having to change all the places in my code where I directly made a tuple.
#
# - square_row(square) and square_col(col) take care of indexing the tuple and
#   returning the respective values. Again, if I use these functions everywhere
#   instead of directly writing square[0] and square[1], I have the flexibility
#   to do things like change squares' implementation, e.g. from (row, col)
#   tuples to (col, row) tuples, by only changing these functions.
#
# - get_square(square) and set_square(square) are the only two functions which
#   actually index board. Both functions convert the row 1-3 to an index 0-2,
#   then convert the column 'A'-'C' to an index 0-2. It's EXTREMELY important
#   that I never index the board directly ANYWHERE else.
#
# With these functions, I have taken care of a huge potential for bugs. Now, I
# no longer have to worry about recognizing that the square (2, 'C') is actually
# board[1][2]. I can just use the abstract concept of squares everywhere.
#
# I used the same abstraction idea for the graphics/display. All the functions
# dealing with displaying the board are in one area. Moreover, I have multiple
# functions (for multiple ideas), but only one function would actually be
# considered "visible" or "public", and the rest are "private" or "hidden".
# This visible function doesn't do anything on its own, only the hidden ones do.
# But, the visible function serves as a middleman -- it calls one of the hidden
# ones. By doing this, I am able to easily switch the style of display by only
# changing one line in the visible function. Take a look to better understand.
#
# The rest of the code should be understandable -- it's all stuff you've seen
# before. If you have any questions, feel free to email me. Enjoy!


from random import *
from string import *


## Constants ##

EMPTY = ' '     # the value of an empty square
PL_1 = 'x'      # player 1's mark
PL_2 = 'o'      # player 2's mark

A = 'A'     # these just make it easier to keep referring to 'A', 'B' and 'C'
B = 'B'
C = 'C'


## State variables ##

board = [[EMPTY, EMPTY, EMPTY],     # board is initially all empty squares,
         [EMPTY, EMPTY, EMPTY],     # implemented as a list of rows,
         [EMPTY, EMPTY, EMPTY]]     # three rows with three squares each

current_player = randint(1, 2)      # randomly choose starting player


## Coordinate system functions ##

def square(row, col):       # squares are represented as tuples of (row, col).
    return (row, col)       # rows are numbered 1 thru 3, cols 'A' thru 'C'.

def square_row(square):     # these two functions save us the hassle of using
    return square[0]        # index values in our code, e.g. square[0]...

def square_col(square):     # from this point on, i should never directly use
    return square[1]        # tuples when working with squares.

def get_square(square):
    """ Returns the value of the given square. """
    row_i = square_row(square) - 1      # from values of 1-3 to values of 0-2
    col_i = ord(square_col(square)) - ord(A)    # ord gives the ASCII number
                                                # (search ASCII on wikipedia!)
    return board[row_i][col_i]  # note how this and set_square are the ONLY
                                # functions which directly use board!

def set_square(square, mark):
    """ Sets the value of the given square. """
    row_i = square_row(square) - 1
    col_i = ord(square_col(square)) - ord(A)
    board[row_i][col_i] = mark  # note how this and get_square are the ONLY
                                # functions which directly use board!

def get_row(row):
    """ Returns the given row as a list of three values. """
    return [get_square((row, A)), get_square((row, B)), get_square((row, C))]

def get_column(col):
    """ Returns the given column as a list of three values. """
    return [get_square((1, col)), get_square((2, col)), get_square((3, col))]

def get_diagonal(corner_square):
    """ Returns the diagonal that includes the given corner square.
    Only (1, A), (1, C), (3, A) and (3, C) are corner squares. """
    if corner_square == (1, A) or corner_square == (3, C):
        return [get_square((1, A)), get_square((2, B)), get_square((3, C))]
    else:
        return [get_square((1, C)), get_square((2, B)), get_square((3, A))]


## Game logic functions ##

def get_mark(player):
    """ Returns the mark of the given player (1 or 2). """
    if player == 1:
        return PL_1
    else:
        return PL_2

def all_squares_filled():
    """ Returns True iff all squares have been filled. """
    for row in range(1, 4):     # range(1, 4) returns the list [1, 2, 3]
        if EMPTY in get_row(row):
            return False    # this row contains an empty square, we know enough
    return True     # no empty squares found, all squares are filled

def player_has_won(player):
    """ Returns True iff the given player (1 or 2) has won the game. """

    # we need to check if there are three of the player's marks in a row,
    # so we'll keep comparing against a list of three in a row.
    MARK = get_mark(player)
    win = [MARK, MARK, MARK]

    # first check horizontal rows
    if get_row(1) == win or get_row(2) == win or get_row(3) == win:
        return True

    # no horizontal row, let's try vertical rows
    if get_column(A) == win or get_column(B) == win or get_column(C) == win:
        return True

    # no vertical either, let's try the diagonals
    if get_diagonal((1, A)) == win or get_diagonal((1, C)) == win:
        return True

    return False    # none of the above, player hasn't won


## Display functions ##

# Display idea 1 -- straight representation
#
#      A   B   C
#                
# 1      |   |  
#     ---+---+---
# 2      |   |   
#     ---+---+---
# 3      |   |   
#

def draw_board_straight():
    """ Returns a straight string representation of the board. """

    # for ease, we'll define all the squares as constants
    A1, A2, A3 = get_square((1, A)), get_square((2, A)), get_square((3, A))
    B1, B2, B3 = get_square((1, B)), get_square((2, B)), get_square((3, B))
    C1, C2, C3 = get_square((1, C)), get_square((2, C)), get_square((3, C))
    
    lines = []
    lines.append("")
    lines.append("     " + A + "   " + B + "   " + C + " ")
    lines.append("              ")
    lines.append("1    " + A1 + " | " + B1 + " | " + C1 + " ")
    lines.append("    ---+---+---")
    lines.append("2    " + A2 + " | " + B2 + " | " + C2 + " ")
    lines.append("    ---+---+---")
    lines.append("3    " + A3 + " | " + B3 + " | " + C3 + " ")
    lines.append("")
    
    return join(lines, '\n')    # the '\n' represents a newline

# Display idea 2 -- slanted representation
# 
#            A   B   C 
#                      
#     1      /   /     
#        ---/---/---   
#   2      /   /       
#      ---/---/---     
# 3      /   /         
# 

def draw_board_slanted():
    """ Returns a slanted string representation of the board. """

    # for ease, we'll define all the squares as constants
    A1, A2, A3 = get_square((1, A)), get_square((2, A)), get_square((3, A))
    B1, B2, B3 = get_square((1, B)), get_square((2, B)), get_square((3, B))
    C1, C2, C3 = get_square((1, C)), get_square((2, C)), get_square((3, C))
    
    lines = []
    lines.append("")
    lines.append("           " + A + "   " + B + "   " + C + " ")
    lines.append("                     ")
    lines.append("    1    " + A1 + " / " + B1 + " / " + C1 + "  ")
    lines.append("       ---/---/---  ")
    lines.append("  2    " + A2 + " / " + B2 + " / " + C2 + "    ")
    lines.append("     ---/---/---    ")
    lines.append("3    " + A3 + " / " + B3 + " / " + C3 + "      ")
    lines.append("")
    
    return join(lines, '\n')    # the '\n' represents a newline

# And now the flexibility of being able to choose either style with one change!
# This is the power of abstraction -- we abstracted away the task of drawing.

def draw_board():
    """ Returns a string representation of the board in its current state. """
    return draw_board_slanted()     # this is the only line we'd have to change.
    #return draw_board_straight()   # in fact, if you want to change it, just
                                    # uncomment one line and comment the other!


## Game functions ##

def reset_board():
    for row in (1, 2, 3):
        for col in (A, B, C):
            set_square(square(row, col), EMPTY)

def play_game():

    global current_player   # we need the global statement to change variables
                            # that are defined OUTSIDE of the current function

    reset_board()
    current_player = randint(1, 2)

    print "Tic-Tac-Toe!"
    print

    player1_name = raw_input("Player 1, what is your name? ")
    player2_name = raw_input("Player 2, what is your name? ")

    # quick helper function to print the given player's name
    def get_name(player):
        if player == 1:
            return player1_name
        else:
            return player2_name

    print
    print "Welcome,", player1_name, "and", player2_name + "!"
    print player1_name, "will be", PL_1 + ", and", player2_name, "will be", PL_2 + "."
    print "By random decision,", get_name(current_player), "will go first."
    print

    raw_input("[Press enter when ready to play.] ")     # just waiting for them to press enter

    print draw_board()

    while not all_squares_filled():

        choice = raw_input(get_name(current_player) + ", which square? (e.g. 2B, 2b, B2 or b2) ")

        if len(choice) != 2:
            print "That's not a square. You must enter a square like b2, or 3C."
            print
            continue

        if choice[0] not in ["1", "2", "3"] and upper(choice[0]) not in [A, B, C]:
            print "The first character must be a row (1, 2 or 3) or column (A, B or C)."
            print
            continue

        if choice[1] not in ["1", "2", "3"] and upper(choice[1]) not in [A, B, C]:
            print "The second character must be a row (1, 2 or 3) or column (A, B or C)."
            print
            continue

        if choice[0] in ["1", "2", "3"] and choice[1] in ["1", "2", "3"]:
            print "You entered two rows! You must enter one row and one column (A, B or C)."
            print
            continue

        if upper(choice[0]) in [A, B, C] and upper(choice[1]) in [A, B, C]:
            print "You entered two columns! You must enter one row (1, 2 or 3) and one column."
            print
            continue

        # if we're here, we have one row and one column, figure out which is which
        if choice[0] in ["1", "2", "3"]:
            row = int(choice[0])
            col = upper(choice[1])
        else:
            row = int(choice[1])
            col = upper(choice[0])

        choice = square(row, col)   # make this into a (row, col) tuple

        if get_square(choice) != EMPTY:
            print "Sorry, that square is already marked."
            print
            continue

        # if we're here, then it's a valid square, so mark it
        set_square(choice, get_mark(current_player))

        print draw_board()

        if player_has_won(current_player):
            print "Congratulations", get_name(current_player), "-- you win!"
            print
            break

        if all_squares_filled():
            print "Cats game!", player1_name, "and", player2_name, "draw."
            print
            break

        # now switch players
        current_player = 3 - current_player     # sets 1 to 2 and 2 to 1

    print "GAME OVER"
    print


## Main program code ##

if __name__ == "__main__":

    keep_playing = True

    while keep_playing:

        play_game()
        again = lower(raw_input("Play again? (y/n) "))

        print
        print
        print

        if again != "y":
            keep_playing = False

    print "Thanks for playing!"
    print

1 comment

David Lambert 15 years, 2 months ago  # | flag

This comment is written in python3 pseudo-code.

1) Data structure: Perhaps you could use a dictionary for the board. The squares would be ``board[choice]''. Use frozenset type for the key so that the order A1 or 1A doesn't matter.

2) Reduction of repeated code: removal of upper function. choice = frozenset(input(prompt).upper())

3) Shorter input validation by combining notes 1 and 2:

if choice not in board:
    print('Valid input looks like this:  ..., try again please')
    continue

4) Reduce repeat code: Implementation calls function all_squares_filled in two places. Consider using the ``else'' feature of while loop.

while not tie():
    choice = frozenset(raw_input(prompt).upper())
    if choice not in board:
        # display valid input message
        continue
    if board[choice] != EMPTY:
        # display square already used message
        continue
    board[choice] = get_mark(current_player) # or use set_square function if you prefer
    print(draw_board(style))
    if winner(current_player):
        # woot!
        break
    # switch players
else:
    # display cat game message
#display game finished

5) with dictionary, all_squares_filled function becomes

def all_squares_filled():
    return EMPTY not in set(board.items())

tie = all_squares_filled

happy programming, Dave.