#Connect 4 Game


Send me on my Way

Implement a console-based game (Connect 4) that the user will initially be able to play on their own computer, but will extend so that they can play via the Internet by connecting to a central, shared server.

'''MODULE 1:''' import connectfour import sharedfunctions def play_game() -> None: '''Runs the main game loop in which two players alternate taking turns.''' game_state = connectfour.new_game() while True: sharedfunctions.display_board(game_state) if sharedfunctions.color_turn(game_state) == connectfour.RED: print("It is Red player's turn.") elif sharedfunctions.color_turn(game_state) == connectfour.YELLOW: print("It is Yellow player's turn.") t = sharedfunctions.user_input() column_number = sharedfunctions.choose_column(t[1]) move_type = sharedfunctions.choose_move_type(t[0]) game_state = sharedfunctions.move(game_state,column_number,move_type) winning_player = connectfour.winner(game_state) if winning_player != connectfour.NONE: sharedfunctions.game_over(winning_player) sharedfunctions.display_board(game_state) break def display_game() -> None: ''' Displays the user interface which asks the user if they want to start a game.''' sharedfunctions.welcome_message() while True: choice = input("Would you like to start a new game? (Y/N)\n") if choice.lower() == 'y': play_game() elif choice.lower() == 'n': break else: print("Invalid choice, please enter 'Y' or 'N'") if __name__ == '__main__': display_game() '''MODULE 2:''' import socket import collections Connection = collections.namedtuple("Connection", "socket socket_input socket_output") def read_host() -> str: '''Asks user for valid host name, and if blank, asks again''' while True: host = input('Host: ').strip() if host == '': print('Please specify a host (either a name or an IP address)') else: return host def read_port() -> int: '''Asks user for valid port number, and if blank or not within range, asks again''' while True: try: port = int(input('Port: ').strip()) if port < 0 or port > 65535: print('Ports must be an integer between 0 and 65535') else: return port except ValueError: print('Ports must be an integer between 0 and 65535') def enter_username() -> str: '''Asks user for a username, and if blank, asks again''' while True: username = input("Choose a username. Please avoid whitespaces: ") if " " in username: enter_username() else: return username def connect(host: str, port: int) -> Connection: '''Connects to the server (while running on the inputted host and listening on the given port); if successful, a connection object is returned, and if unsuccessful, an exception is raised''' new_socket = socket.socket() new_socket.connect((host,port)) new_socket_in = new_socket.makefile('r') new_socket_out = new_socket.makefile('w') return Connection(new_socket, new_socket_in, new_socket_out) def close(connection: Connection) -> None: '''Closes a connection''' connection.socket_input.close() connection.socket_output.close() connection.socket.close() def send_message(connection: Connection, message: str) -> None: '''Sends a message to the server via a connection that is already assumed to have been opened (and not yet closed).''' connection.socket_output.write(message + '\r\n') connection.socket_output.flush() def read_message(connection: Connection) -> str: '''Receives a response from the server via a connection that is already assumed to have been opened (and not yet closed).''' return connection.socket_input.readline()[:-1] def start_game(connection: Connection) -> None: '''Sends a message to the server in order to start a new game.''' playermove = "AI_GAME" send_message(connection, playermove) def login(connection: Connection, username: str) -> None: '''Sends a message to the server in order to login with a username.''' send_message(connection, "I32CFSP_HELLO "+username) '''MODULE 3:''' import connectfour class RangeError(Exception): '''Raised whenever an invalid column number is called''' pass class MoveError(Exception): '''Raised whenever a word other than drop or pop is called''' pass def welcome_message() -> None: '''Displays a welcome message to the player(s)''' print("Welcome to Connect Four!") def display_board(game_state: connectfour.GameState) -> str: '''Displays the board through the game state in the console''' x = 1 for num in range(connectfour.BOARD_COLUMNS): print(x, end=' ') x += 1 print() for i in range(connectfour.BOARD_ROWS): for j in range(connectfour.BOARD_COLUMNS): if game_state.board[j][i] == connectfour.NONE: print(". ", end = "") else: if (game_state.board[j][i]) == 1: print("R ", end = "") else: print("Y ", end = "") print() def user_input() -> str: '''Asks the player to choose their move and column number to drop their coin''' while True: move = '' colNum = '' try: user_input = input("Enter if you would like to drop or pop your coin. Then, select a number between 1 and %d for your turn:\n" % (connectfour.BOARD_COLUMNS)) split = user_input.split() move = split[0] colNum = int(split[1]) if (colNum > 0 and colNum <= connectfour.BOARD_COLUMNS) == False: raise RangeError if ((move == 'drop') or (move == 'pop')) == False: raise MoveError return move, colNum except MoveError: print("Invalid input: Not drop or pop") except RangeError: print("Invalid input: Number not within range") pass except IndexError: print("Invalid input: Please enter 'drop' or 'pop' followed by a number") pass except ValueError: print("Invalid input: No number inputted") pass except OSError: print("Invalid input") pass except (move != 'drop') or (move != 'pop'): print("Invalid input: not pop or drop") pass def choose_column(number) -> int: '''Translates user input of a number to a specific column number''' colNum = number return colNum def choose_move_type(move) -> str: '''Determines if the value entered for move (drop or pop) is valid or not if (move == 'drop') or (move == 'pop'):''' return move def color_turn(game_state: connectfour.GameState) -> str: '''Changes players back and forth''' return game_state.turn def move(game_state: connectfour.GameState, column_number: int, move_type: str) -> None: '''Applies a move to the game state.''' try: if move_type == 'drop': return connectfour.drop(game_state,column_number-1) elif move_type == 'pop': return connectfour.pop(game_state,column_number-1) except connectfour.InvalidMoveError: print("Invalid move.") return game_state except connectfour.GameOverError: print("The game is already over!") return game_state def game_over(player: str) -> None: '''Checks if a player has won.''' if player == connectfour.RED: print("Red has won, the game is now over.") elif player == connectfour.YELLOW: print("Yellow has won, the game is now over.") '''MODULE 4:''' # connectfour.py # # ICS 32 Winter 2019 # Project #2: Send Me On My Way ''' This module contains the game logic that underlies a Connect Four game, implementing such functionality as tracking the state of a game, updating that state as players make moves, and determining if there is a winner. No user interface or network functionality is included; this is strictly a collection of tools for implementing the game logic. ''' import collections # These constants specify the concepts of "no player", the "red player" # and the "yellow player". Your code should use these constants in # place of their hard-coded values. Note that these values are not # intended to be displayed to a user; they're simply intended as a # universal way to distinguish which player we're talking about (e.g., # which player's turn it is, or which player (if any) has a piece in a # particular cell on the board). NONE = 0 RED = 1 YELLOW = 2 # These constants specify the size of the game board (i.e., the number # of rows and columns it has). It should be possible to change these # constants and re-run your program so that the board will have a # different size; be sure that you're not hard-coding these values # anywhere in your own code, because this is something we'll be # attempting when we test your program. BOARD_COLUMNS = 7 BOARD_ROWS = 6 # GameState is a namedtuple that tracks everything important about # the state of a Connect Four game as it progresses. It contains # two fields: # # 'board', which is a two-dimensional list of values describing # the game board. Each value represents one cell on the board # and is either NONE, RED, or YELLOW, depending on whether the # cell contains a red piece, a yellow piece, or is empty # # 'turn', which specifies which player will make the next move; # its value will always be either RED or YELLOW GameState = collections.namedtuple('GameState', ['board', 'turn']) # This is the simplest example of how you create new kinds of exceptions # that are specific to your own program. We'll see later in the quarter # in more detail what this notation means; in short, we're introducing # new types into our program and stating that they have strong similarities # to the existing type called Exception. class InvalidMoveError(Exception): '''Raised whenever an invalid move is made''' pass class GameOverError(Exception): ''' Raised whenever an attempt is made to make a move after the game is already over ''' pass # The next four functions are the "public" functions that are provided # by this module. You'll need to call all four of these functions, but # will not need (or want) to call any of the others. def new_game() -> GameState: ''' Returns a GameState representing a brand new game in which no moves have been made yet. ''' return GameState(board = _new_game_board(), turn = RED) def drop(game_state: GameState, column_number: int) -> GameState: ''' Given a game state and a column number, returns the game state that results when the current player (whose turn it is) drops a piece into the given column. If the column number is invalid, a ValueError is raised. If the game is over, a GameOverError is raised. If a move cannot be made in the given column because the column is filled already, an InvalidMoveError is raised. ''' _require_valid_column_number(column_number) _require_game_not_over(game_state) empty_row = _find_bottom_empty_row_in_column(game_state.board, column_number) if empty_row == -1: raise InvalidMoveError() else: new_board = _copy_game_board(game_state.board) new_board[column_number][empty_row] = game_state.turn new_turn = _opposite_turn(game_state.turn) return GameState(board = new_board, turn = new_turn) def pop(game_state: GameState, column_number: int) -> GameState: ''' Given a game state and a column number, returns the game state that results when the current player (whose turn it is) pops a piece from the bottom of the given column. If the column number is invalid, a ValueError is raised. If the game is over, a GameOverError is raised. If a piece cannot be popped from the bottom of the given column because the column is empty or because the piece at the bottom of the column belongs to the other player, an InvalidMoveError is raised. ''' _require_valid_column_number(column_number) _require_game_not_over(game_state) if game_state.turn == game_state.board[column_number][BOARD_ROWS - 1]: new_board = _copy_game_board(game_state.board) for row in range(BOARD_ROWS - 1, -1, -1): new_board[column_number][row] = new_board[column_number][row - 1] new_board[column_number][row] = NONE new_turn = _opposite_turn(game_state.turn) return GameState(board = new_board, turn = new_turn) else: raise InvalidMoveError() def winner(game_state: GameState) -> int: ''' Determines the winning player in the given game state, if any. If the red player has won, RED is returned; if the yellow player has won, YELLOW is returned; if no player has won yet, NONE is returned. ''' winner = NONE for col in range(BOARD_COLUMNS): for row in range(BOARD_ROWS): if _winning_sequence_begins_at(game_state.board, col, row): if winner == NONE: winner = game_state.board[col][row] elif winner != game_state.board[col][row]: # This handles the rare case of popping a piece # causing both players to have four in a row; # in that case, the last player to make a move # is the winner. return _opposite_turn(game_state.turn) return winner # Modules often contain functions, variables, or classes whose names begin # with underscores. This is no accident; in Python, this is the agreed-upon # standard for specifying functions, variables, or classes that should be # treated as "private" or "hidden". The rest of your program should not # need to call any of these functions directly; they are utility functions # called by the functions above, so that those functions can be shorter, # simpler, and more readable. def _new_game_board() -> [[int]]: ''' Creates a new game board. Initially, a game board has the size BOARD_COLUMNS x BOARD_ROWS and is comprised only of integers with the value NONE ''' board = [] for col in range(BOARD_COLUMNS): board.append([]) for row in range(BOARD_ROWS): board[-1].append(NONE) return board def _copy_game_board(board: [[int]]) -> [[int]]: '''Copies the given game board''' board_copy = [] for col in range(BOARD_COLUMNS): board_copy.append([]) for row in range(BOARD_ROWS): board_copy[-1].append(board[col][row]) return board_copy def _find_bottom_empty_row_in_column(board: [[int]], column_number: int) -> int: ''' Determines the bottommost empty row within a given column, useful when dropping a piece; if the entire column in filled with pieces, this function returns -1 ''' for i in range(BOARD_ROWS - 1, -1, -1): if board[column_number][i] == NONE: return i return -1 def _opposite_turn(turn: str) -> str: '''Given the player whose turn it is now, returns the opposite player''' if turn == RED: return YELLOW else: return RED def _winning_sequence_begins_at(board: [[int]], col: int, row: int) -> bool: ''' Returns True if a winning sequence of pieces appears on the board beginning in the given column and row and extending in any of the eight possible directions; returns False otherwise ''' return _four_in_a_row(board, col, row, 0, 1) \ or _four_in_a_row(board, col, row, 1, 1) \ or _four_in_a_row(board, col, row, 1, 0) \ or _four_in_a_row(board, col, row, 1, -1) \ or _four_in_a_row(board, col, row, 0, -1) \ or _four_in_a_row(board, col, row, -1, -1) \ or _four_in_a_row(board, col, row, -1, 0) \ or _four_in_a_row(board, col, row, -1, 1) def _four_in_a_row(board: [[int]], col: int, row: int, coldelta: int, rowdelta: int) -> bool: ''' Returns True if a winning sequence of pieces appears on the board beginning in the given column and row and extending in a direction specified by the coldelta and rowdelta ''' start_cell = board[col][row] if start_cell == NONE: return False else: for i in range(1, 4): if not _is_valid_column_number(col + coldelta * i) \ or not _is_valid_row_number(row + rowdelta * i) \ or board[col + coldelta *i][row + rowdelta * i] != start_cell: return False return True def _require_valid_column_number(column_number: int) -> None: '''Raises a ValueError if its parameter is not a valid column number''' if type(column_number) != int or not _is_valid_column_number(column_number): raise ValueError('column_number must be int between 0 and {}'.format(BOARD_COLUMNS - 1)) def _require_game_not_over(game_state: GameState) -> None: ''' Raises a GameOverError if the given game state represents a situation where the game is over (i.e., there is a winning player) ''' if winner(game_state) != NONE: raise GameOverError() def _is_valid_column_number(column_number: int) -> bool: '''Returns True if the given column number is valid; returns False otherwise''' return 0 <= column_number < BOARD_COLUMNS def _is_valid_row_number(row_number: int) -> bool: '''Returns True if the given row number is valid; returns False otherwise''' return 0 <= row_number < BOARD_ROWS
1