# connectfour.py # A two-player command-line implementation of Connect Four. # # Author: Aseem Kishore # # License: None -- free to use. Just give credit where it's due please! =) from string import lower, upper, join from random import randint ## Constants ## NUM_ROWS = 6 NUM_COLS = 7 ROWS = range(1, NUM_ROWS + 1) COLS = [chr(i) for i in range(ord("A"), ord("A") + NUM_COLS)] EMPTY = " " PL_1 = "x" PL_2 = "o" NEEDED_TO_WIN = 4 # number in-a-row needed to win ## Board implementation ## # The board is implemented as a list of columns. Each column is implemented # as a list of tokens. The length of each of these column lists is exactly # the number of tokens in that column. So initially, each column list is # empty. When a token is added, we'll append that to the end of that list, # so this means that the bottom token is always index 0. # # The abstract representation of the board is a grid with numbered rows and # lettered columns. The bottom row is row 1, then row 2, etc. The position # of a particular square is then a (row, col) tuple, so the position (1, A) # refers to the bottom-left square. _board = [ [] for i in range(NUM_COLS) ] # list of 7 columns, each list # initially empty def _assert_valid_position(position): assert type(position) is tuple and len(position) == 2,\ "position must be a (row, col) tuple, %s given" % position _assert_valid_row(position[0]) _assert_valid_col(position[1]) def _assert_valid_row(row): assert row in ROWS,\ "row must be an integer in the range %i-%i, %s given" %\ (ROWS[0], ROWS[-1], row) def _assert_valid_col(col): assert type(col) is str and upper(col) in COLS,\ "col must be a letter in the range %c-%c, %s given" %\ (COLS[0], COLS[-1], col) def _col_num(col): """ Returns the column number (1, 2, ...) of the given column (A, B, ...). """ return COLS.index(upper(col)) + 1 def position(row, col): """ Returns the position at the given row and column. """ _assert_valid_row(row) _assert_valid_col(col) return (row, col) def row(position): """ Returns the row of the given position. """ _assert_valid_position(position) return position[0] def col(position): """ Returns the column of the given position. """ _assert_valid_position(position) return position[1] def get_square(position): """ Returns the square (EMPTY, PL_1 or PL_2) at the given position. """ _assert_valid_position(position) row_i = row(position) - 1 col_i = _col_num(col(position)) - 1 column = _board[col_i] if row_i >= len(column): return EMPTY return column[row_i] def get_row(row): """ Returns a list of squares (EMPTY, PL_1 or PL_2) in the given row. The first square is at column A, the next at column B, etc. """ _assert_valid_row(row) row_i = row - 1 squares = [] for col_i in range(NUM_COLS): column = _board[col_i] if row > len(column): squares.append(EMPTY) else: squares.append(column[row_i]) assert len(squares) == NUM_COLS,\ "INTERNAL ERROR: squares should be %i long, %i long instead" %\ (NUM_COLS, len(squares)) return squares def get_column(col): """ Returns a list of squares (EMPTY, PL_1 or PL_2) in the given column. The first square is at row 1, the next at row 2, etc. """ _assert_valid_col(col) col_i = _col_num(col) - 1 squares = list(_board[col_i]) while len(squares) < NUM_ROWS: squares.append(EMPTY) return squares def get_diagonal_TL_to_BR(tl_position): """ Returns the top-left to bottom-right diagonal that begins at the given top-left position (meaning a position on the top or left edges). """ _assert_valid_position(tl_position) assert row(tl_position) == ROWS[-1] or col(tl_position) == COLS[0],\ ("tl_position must be on the top (row %i) or left (col %c)" +\ " edges, %s given") % (ROWS[-1], COLS[0], tl_position) squares = [] row_i = row(tl_position) - 1 col_i = _col_num(col(tl_position)) - 1 while row_i >= 0 and col_i < NUM_COLS: column = _board[col_i] if row_i >= len(column): squares.append(EMPTY) else: squares.append(column[row_i]) row_i -= 1 # moving down, i.e. row_i is decreasing col_i += 1 # moving right, i.e. col_i is increasing assert len(squares) >= 1,\ "INTERNAL ERROR: squares is empty" assert len(squares) <= min(NUM_ROWS, NUM_COLS),\ "INTERNAL ERROR: should have at most %i squares, have %i" %\ (min(NUM_ROWS, NUM_COLS), len(squares)) return squares def get_diagonal_BL_to_TR(bl_position): """ Returns the bottom-left to top-right diagonal that begins at the given bottom-left position (meaning a position on the bottom or left edges). """ _assert_valid_position(bl_position) assert row(bl_position) == ROWS[0] or col(bl_position) == COLS[0],\ ("bl_position must be on the bottom (row %i) or left (col %c)" +\ " edges, %s given") % (ROWS[0], COLS[0], bl_position) squares = [] row_i = row(bl_position) - 1 col_i = _col_num(col(bl_position)) - 1 while row_i < NUM_ROWS and col_i < NUM_COLS: column = _board[col_i] if row_i >= len(column): squares.append(EMPTY) else: squares.append(column[row_i]) row_i += 1 # moving up, i.e. row_i is increasing col_i += 1 # moving right, i.e. col_i is increasing assert len(squares) >= 1,\ "INTERNAL ERROR: squares is empty" assert len(squares) <= min(NUM_ROWS, NUM_COLS),\ "INTERNAL ERROR: should have at most %i squares, have %i" %\ (min(NUM_ROWS, NUM_COLS), len(squares)) return squares def drop_into_column(col, mark): """ Drops a token with the given mark into the given column. Raises an Exception if the given column is filled. """ _assert_valid_col(col) col_i = _col_num(col) - 1 column = _board[col_i] if len(column) == NUM_ROWS: raise Exception, "column %c is filled" % col column.append(mark) def board_filled(): """ Returns True iff the board has been completely filled. """ for col_i in range(NUM_COLS): if len(_board[col_i]) < NUM_ROWS: return False return True def clear_board(): """ Clears the board so that all squares are EMPTY. """ for col_i in range(NUM_COLS): _board[col_i] = [] ## Game logic ## def enough_to_win(mark): """ Returns True iff the board contains enough in-a-row of the given mark needed to win. All rows, columns and diagonals are considered. """ N = NEEDED_TO_WIN # the number we need to win, e.g. 4 win = [mark for i in range(N)] # to compare win against a list of squares, we'll go square by square # and when we find one that is our mark, we'll slice it to look at the # next N-1 elements also, then compare that against win def contains_win(squares): for i in range(len(squares)): if squares[i] == mark: if squares[i:i+N] == win: return True return False # go row by row first for row in ROWS: squares = get_row(row) if contains_win(squares): return True # same for columns for col in COLS: squares = get_column(col) if contains_win(squares): return True # diagonals are a bitch. we'll try all the TL-to-BR diagonals first. # try all the left-edge positions first, then all the top-edge ones. col = COLS[0] for row in ROWS: squares = get_diagonal_TL_to_BR(position(row, col)) if contains_win(squares): return True row = ROWS[-1] for col in COLS: squares = get_diagonal_TL_to_BR(position(row, col)) if contains_win(squares): return True # and same for all the BL-to-TR diagonals. left then bottom edges. col = COLS[0] for row in ROWS: squares = get_diagonal_BL_to_TR(position(row, col)) if contains_win(squares): return True row = ROWS[0] for col in COLS: squares = get_diagonal_BL_to_TR(position(row, col)) if contains_win(squares): return True # nothing found in rows, columns or diagonals return False ## Display ## # # a b c d e f g # # . . . . . . . # . . . . . . . # . . . . . . . # . . . . . . . # . . o x . . . # . o x o x . . # def _get_simple_display(): # see commented display above to understand the process lines = [] lines.append("") # column letters col_letters = [lower(col) for col in COLS] lines.append(join(col_letters)) # space between each letter lines.append("") # each row will display each square in that row, from top to bottom ROWS_REVERSED = list(ROWS) ROWS_REVERSED.reverse() for row in ROWS_REVERSED: row_letters = [] for square in get_row(row): if square == EMPTY: row_letters.append(".") else: row_letters.append(square) lines.append(join(row_letters)) # space between each letter lines.append("") return lines # # A B C D E F G # __ __ # || | | | | | | || # |+---+---+---+---+---+---+---+| # || | | | | | | || # |+---+---+---+---+---+---+---+| # || | | | | | | || # |+---+---+---+---+---+---+---+| # || | | | | | | || # |+---+---+---+---+---+---+---+| # || | | | | | | || # |+---+---+---+---+---+---+---+| # || o | o | x | o | x | x | o || # |+---+---+---+---+---+---+---+| # ^^ ^^ # def _get_fancy_display(): # see commented display above to understand the process lines = [] lines.append("") # column letters col_letters = [] for col in COLS: col_letters.append(" %c " % col) # space, letter, space lines.append(" " + join(col_letters) + " ") # join here puts a space # between each element, but we also need a space for the first # column's left edge and the last column's right edge lines.append("__" + join([" "] * NUM_COLS) + "__") # each row will now display its own left, right and bottom edges, # but we need to do it from top down instead of bottom up ROWS_REVERSED = list(ROWS) ROWS_REVERSED.reverse() for row in ROWS_REVERSED: # each square will display its own left and bottom edges row_letters_1 = [] # e.g. | X | | O | X ... row_letters_2 = [] # e.g. +---+---+---+--- row_letters_1.append("|") # for the left-most outer edge row_letters_2.append("|") squares = get_row(row) for square in squares: row_letters_1.append("| %c " % square) row_letters_2.append("+---") row_letters_1.append("||") # for the right-most outer edge row_letters_2.append("+|") lines.append(join(row_letters_1, "")) # no space between each lines.append(join(row_letters_2, "")) lines.append("^^" + join([" "] * NUM_COLS) + "^^") lines.append("") return lines def get_display(): """ Returns a list of lines that represent the display of the board. """ #return _get_simple_display() return _get_fancy_display() ## Main game code ## def play_game(): clear_board() current_player = randint(1, 2) # randomly decide who's first # quick helper function to get the given player's mark def get_mark(player): if player == 1: return PL_1 else: return PL_2 print "Connect Four!" 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 get the given player's name def get_name(player): if player == 1: return player1_name else: return player2_name print print "Welcome, %s and %s!" % (player1_name, player2_name) print "%s will be %s, and %s will be %s." %\ (player1_name, PL_1, player2_name, PL_2) print "By coinflip, %s will go first." % get_name(current_player) print raw_input("[Press enter when ready to play.] ") # quick helper function to print the board def print_board(): print join(get_display(), "\n\t") print_board() while not board_filled(): name = get_name(current_player) mark = get_mark(current_player) prompt = "%s (%c), which column? (e.g. A, a, B, b) " % (name, mark) choice = raw_input(prompt) if len(choice) != 1 or upper(choice) not in COLS: print ("A column is one letter, from %c to %c." %\ (COLS[0], COLS[-1])), "Lowercase is fine." print "Please try again." print continue if get_column(choice)[-1] != EMPTY: print "Sorry, column %c is already filled." % choice print "Please try again." print continue drop_into_column(choice, mark) print_board() if enough_to_win(mark): print "Congratulations, %s -- you win!" % name print break if board_filled(): print "The board is filled. The game ends in a draw." print break # now switch players current_player = 3 - current_player # sets 1 to 2 and 2 to 1 print "Game Over" print if __name__ == "__main__": keep_playing = True while keep_playing: print "+-" * 37 + "+" print play_game() again = lower(raw_input("Play again? (y/n) ")) print if lower(again) != "y": keep_playing = False print "Thanks for playing!" print