def game_handler(self): """ This function controls the game. if the game is running and there is no winner yet, checks if the player is ai or human. if the player is ai it preforms the ai move by calling the functions "find legal move" from the AI class and the "make move" function from the Game class. if there is a winner' the function presents the winner by calling the "win state" function. """ if self.__game_is_running: state = self.game.get_winner() current_player = self.game.get_current_player() self.update_info(current_player) if state is not None: self.__game_is_running = False self.win_state(state) else: tk.Label(self.__info_frame, text='Player {} turn!'.format(current_player)).place( x=350, y=0) if self.__players[current_player]: comp = AI(self.game, current_player) try: chosen_move = comp.find_legal_move() # Sleeping for 0.5 second before making the computer move sleep(0.5) self.__make_move(chosen_move) # If exception is returned - The game is finished, using pass for moving to move forward # (The finished game situation will be managed by the next call of game_handler) except Exception: pass self.__parent.after(100, self.game_handler)
class FourInARow: """ The high level application object, handling game events, turn order and legality, communication between instances and controlling the objects that manage GUI, gameplay and AI. """ PLAYERS = ARG_PLAYERS MSG_NOT_TURN = 'Not your turn!' MSG_DRAW = 'draw' MSG_WIN = 'winner!' MSG_LOSE = 'loser :(' def __init__(self, root, player, port, ip): """ The function initialize all object's private values. :param player: A string which decide if the player is human or ai. :param port: An integer between 0 to 65535. better use ~8000 :param ip: The host IP. can be None if the player is the host or the host ip address. """ self.__root = root self.__game = Game() # decide whether the player is AI or not if player == self.PLAYERS[1]: self.__is_ai = True else: self.__is_ai = False # Set both Players colors if ip: self.__my_color = Game.PLAYER_TWO self.__op_color = Game.PLAYER_ONE else: self.__my_color = Game.PLAYER_ONE self.__op_color = Game.PLAYER_TWO self.__communicator = Communicator(root, port, ip) self.__communicator.connect() self.__communicator.bind_action_to_message(self.handle_message) if self.__is_ai: # If the player is AI we initialize an AI object self.__screen = Screen(root, self.__my_color, lambda y: None) self.__ai = AI() if self.__my_color == Game.PLAYER_ONE: self.ai_find_move( ) # and call ai_find_move to make the first move. else: self.__screen = Screen(root, self.__my_color, self.play_my_move) def ai_find_move(self): """ The function handles the AI turn. It creates a copy of the game object and sends it to the AI instance. Then, it makes the next AI move using the AI find_legal_move method. :return: None """ # creates a copy of the game instance and sends it to the AI. sim_game = Game() board, register, cell_set, counter = self.__game.get_attr_for_sim() sim_game.set_attr_for_sim(board, register, cell_set, counter) try: self.__ai.find_legal_move(sim_game, self.play_my_move) except: self.__screen.print_to_screen(self.__ai.NO_AI_MOVE, self.__my_color) def play_my_move(self, column): """ The function handles a certain game move, both by the AI instance and when a GUI button is pressed, by calling the class one_turn method and sending the opponent a message using the communicator instance. :param column: column in board to play (0 <= int <= 6) :return: None """ if self.one_turn(column, self.__my_color): self.__communicator.send_message(str(column)) def one_turn(self, column, player): """ The function handles one turn of both kinds of players, AI and human by preforming the following actions: 1. Try to make the given move(column). 2. Update the screen instance according to the move. 3. Send the opponent an message about the move it made. 4. Checks if the game ended using the game get_winner method. :param column: column in board (0 <= int <= 6) :param player: The player which made the turn. Player_one/Player_two. :return: True if the move was done (may be illegal move). None otherwise. """ # The below if make sure that both players can play only when its their turn. if self.__game.get_current_player() == player: #Try to make the move, if the move is illegal raise an exception try: self.__game.make_move(column) row, col = self.__game.get_last_coord() move_done = True if self.__is_ai: self.__screen.update_cell(row, col, player, anim=False) else: self.__screen.update_cell(row, col, player, anim=True) except: self.__screen.print_to_screen(self.__game.ILLEGAL_MOVE_MSG, player) return else: self.__screen.print_to_screen(self.MSG_NOT_TURN, player) return # check if the game is ended by win/loss/draw. winner = self.__game.get_winner() if winner is not None: self.end_game(winner) return move_done def end_game(self, winner): """ The function handles the situation where the game is done. Its using the screen instance to print the game result (win/loss/draw) to the graphical interface. :param winner: The game result (PLAYER_ONE / PLAYER_TWO / DRAW). :return: None """ win_coord, win_dir = self.__game.get_win_info( ) # Ask the game instance for the win_coord and direction self.__screen.win( win_coord, win_dir, winner) # In order to display the winning sequence (FLASH!) if winner == Game.DRAW: self.__screen.print_to_screen(self.MSG_DRAW, self.__my_color, end=True) self.__screen.print_to_screen(self.MSG_DRAW, self.__op_color, end=True) elif winner == self.__my_color: self.__screen.print_to_screen(self.MSG_WIN, self.__my_color, end=True) self.__screen.print_to_screen(self.MSG_LOSE, self.__op_color, end=True) elif winner == self.__op_color: self.__screen.print_to_screen(self.MSG_WIN, self.__op_color, end=True) self.__screen.print_to_screen(self.MSG_LOSE, self.__my_color, end=True) def handle_message(self, message=None): """ The function specifies the event handler for the message getting event in the communicator. When it is invoked, it calls the one_turn method in order to update the opponent move on its screen instance or end the game if needed. it invoked by the communicator when a message is received. :param message: The last move the opponent made (0 <= int <= 6) Default is None. :return: None """ if message: self.one_turn(int(message[0]), self.__op_color) if self.__is_ai: # If the player is AI we call ai_find_move to make the AI next move. if self.__game.get_win_info()[1] is None: self.ai_find_move()
class Gui(): """ class of the Gui wrapping the game """ SUM_OF_ALL_BOARD_CELLS = 7 * 6 RANGE_BALL_CLICK = 60 END_BARRIER_AXIS_X = 435 START_BARRIER_BALLS_AXIS_X = 15 END_OF_AXIS_Y_BALLS = 440 START_SPACE_AXIS_Y = 90 START_SPACE_AXIS_X = 20 SPLITTER_OF_BALLS_AXIS_Y = 60 SPLITTER_OF_BALLS_AXIS_X = 60 NUM_OR_ROWS = 6 NUM_OF_COLUNM = 7 CANVAS_SIZE = 450 BALL_SIZE = 50 STEP_SIZE = 2 WINNER_FONT = ("Helvetica", 20) WINNER_MSG = "The winner is the" BACKGROUND_COL = "#6495ED" DEFAULT_DISC_COL = 'white' PLAYER1_COL = "red" WIN_COL = 'yellow' PLAYER2_COL = "#008000" NO_WIN_MSG = "no one wins" WIN_MSG = "You win" LOOSE_MSG = 'You loose' BEST_AI_DEFAULT = 0 YOUR_TURN_LABEL = 'For playing your turn\nclick on the wanted column' LABEL_COLOR = 'white' LABEL_X_PLACE = 65 LABEL_Y_PLACE = 15 def __init__(self, parent, port, is_ai, game, ip=None): """ constructor of Gui :param parent: root of the platform :param port: to conect :param is_ai: True if this player is the computer :param ip: of the server. """ # self.game = game self.best_ai_move = self.BEST_AI_DEFAULT self.ip = ip # server begins the game if not ip: self.__my_turn = True else: self.__my_turn = False # initial interface self._parent = parent self._canvas = tk.Canvas(parent, width=self.CANVAS_SIZE, height=self.CANVAS_SIZE, bg=self.BACKGROUND_COL) self._disc_places = [] self._prepeare_canvas() if is_ai: self.is_ai = True self.ai = AI() else: self.is_ai = False self._canvas.bind("<Button-1>", self.click_bind) self._canvas.pack() # communication: self.__communicator = Communicator(parent, port, ip) self.__communicator.connect() self.__communicator.bind_action_to_message(self.__handle_message) # if self is server and ai then responds immediately if is_ai and not ip: self.modify_my_turn(True) self.respond() if not is_ai: self.__put_labal() def __put_labal(self): self.__label = tk.Label(self._parent, text=self.YOUR_TURN_LABEL, fg=self.LABEL_COLOR, font=("Garamond", 20, "bold"), bg=self.BACKGROUND_COL) self.__label.pack() self.__label.place(x=self.LABEL_X_PLACE, y=self.LABEL_Y_PLACE) def __handle_message(self, col=None): """ sends the massage of the player from the other side to the board :param col: number of column :return: None """ self.game.make_move(int(col)) self._update_interface() if self.game.get_winner()[0] != self.game.NO_WINNER: self.declare_winner() return self.modify_my_turn(True) if self.is_ai: self.respond() def click_bind(self, event): """ for every click of the left button of the mouse the func interperts it to the column that the player wanted :param event: the coordinates of the click on the canvas :return: transfer the column to the func board.make_move() """ if self.is_my_turn(): self.game.set_turn(True) if self.START_SPACE_AXIS_Y <= event.y <= self.END_OF_AXIS_Y_BALLS and \ self.START_BARRIER_BALLS_AXIS_X <= event.x <= self.END_BARRIER_AXIS_X: col = (event.x - self.START_BARRIER_BALLS_AXIS_X ) // self.RANGE_BALL_CLICK self.respond(col) def respond(self, col=None): """make the next current player move""" if not self.is_ai: try: column = self.game.make_move(col) self.send_to_other_player(col) self._update_interface() winner = self.game.get_winner()[0] if winner != self.game.NO_WINNER and not self.is_ai: self._canvas.unbind("<Button-1>") self.declare_winner() self.modify_my_turn(False) except: return None else: col = self.ai.find_legal_move(self.game, self.ai_func) self._update_interface() if self.game.get_winner()[0] != self.game.NO_WINNER: self.declare_winner() self.send_to_other_player(col) self.modify_my_turn(False) def ai_func(self, column): """The next function is not necessary to the course team It's the function which records every potential ai moves it the ai will finish all moves it will put -1 in the function and the ouput will be the last ai move""" if column not in range(len(self.game.board[0])): return self.game.make_move(self.best_ai_move) else: self.best_ai_move = column def _update_interface(self): """updates Gui interface so it will fit the acutal board""" for col in range(len(self.game.board[0])): for row in range(len(self.game.board)): if self.game.board[row][col] == self.game.PLAYER_ONE: self.paint_the_disc(col, row, self.game.PLAYER_ONE) elif self.game.board[row][col] == self.game.PLAYER_TWO: self.paint_the_disc(col, row, self.game.PLAYER_TWO) def _prepeare_canvas(self): """ paints on the canvas ovals(discs) :return: None """ balls = [] for col_x in range(self.NUM_OF_COLUNM): for row_y in range(self.NUM_OR_ROWS): x = self.START_SPACE_AXIS_X + self.SPLITTER_OF_BALLS_AXIS_X * col_x y = self.START_SPACE_AXIS_Y + self.SPLITTER_OF_BALLS_AXIS_Y * row_y self._canvas.create_oval(x, y, x + self.BALL_SIZE, y + self.BALL_SIZE, fill=self.DEFAULT_DISC_COL) balls.append((x, y)) self._disc_places.append(balls) balls = [] def paint_the_disc(self, col_x, row_y, player, color=None): """ the func gets the place of the disc that suppose to be paint, by the color of the player :param col_x: :param row_y: :return: """ x = self.START_SPACE_AXIS_X + self.SPLITTER_OF_BALLS_AXIS_X * col_x y = self.START_SPACE_AXIS_Y + self.SPLITTER_OF_BALLS_AXIS_Y * row_y if not color: if player == self.game.PLAYER_ONE: color = self.PLAYER1_COL else: color = self.PLAYER2_COL self._canvas.create_oval(x, y, x + self.BALL_SIZE, y + self.BALL_SIZE, fill=color) def declare_winner(self): """when a player wins it adds title Who is the winner""" winner_player = self.game.get_winner() if winner_player[0] == self.game.PLAYER_ONE: winner = self.WIN_MSG elif winner_player[0] == self.game.PLAYER_TWO: winner = self.LOOSE_MSG else: winner = self.NO_WIN_MSG if winner_player[0] != self.game.DRAW: for ball in winner_player[1]: self.paint_the_disc(ball[1], ball[0], winner_player[0], color=self.WIN_COL) label = tk.Label(self._parent, text=winner, font=self.WINNER_FONT) label.pack(side=tk.TOP) def send_to_other_player(self, column): """ sends to the other player the column that I clicked :param column: :return: """ if self.__my_turn: self.__communicator.send_message(column) def modify_my_turn(self, is_my_turn): """ boolenic changer if it's my turn or not. :param is_my_turn: True or False :return: """ self.__my_turn = is_my_turn self.game.set_turn(is_my_turn) def is_my_turn(self): """ :return: Ture or False if it's my turn """ return self.__my_turn
class GUI: ELEMENT_SIZE = 50 MESSAGE_DISPLAY_TIMEOUT = 250 GRID_COLOR = "#AAA" PLAYER_1_COLOR = "blue" PLAYER_2_COLOR = "red" RED_WIN_COLOR = "#B22222" BLUE_WIN_COLOR = "#008080" DEFAULT_COLOR = "white" BACKGROUND_COLOR = "green" def __init__(self, root, game, human_or_ai, port=None, ip=None): """ Initializes the GUI and connects the communicator. :param parent: the tkinter root. :param ip: the ip to connect to. :param port: the port to connect to. :param server: true if the communicator is a server, otherwise false. """ self.game = game self.ai = AI() self.root = root if human_or_ai: self.ai_on = False else: self.ai_on = True self.ip = ip self.port = port self.my_turn = True """The top image in the gui""" image_path = r"intro2cs.gif" photo = PhotoImage(file=image_path) label = Label(image=photo) label.image = photo # keep a reference label.grid(row=0, column=0, pady=10) self.canvas = Canvas( root, width=200, height=50, background=self.BACKGROUND_COLOR, highlightthickness=0, ) self.canvas.grid(row=2) self.current_player_var = StringVar(self.root, value="") self.currentPlayerLabel = Label(self.root, textvariable=self.current_player_var, anchor=W) self.currentPlayerLabel.grid(row=3) """when the user click on the canvas do action according to the _action_when_canvas_clicked function""" self.canvas.bind('<Button-1>', self._action_when_canvas_clicked) self.new_game() self.__communicator = Communicator(root, port, ip) self.__communicator.connect() self.__communicator.bind_action_to_message(self.__handle_message) def __handle_message(self, text=None): """ Specifies the event handler for the message getting event in the communicator. Prints a message when invoked (and invoked by the communicator when a message is received). The message will automatically disappear after a fixed interval. :param text: the text to be printed. :return: None. """ """If got a text - column, do that move in the self board, so the opponent board and the self board would be synchronized""" if text: self.game.make_move(int(text[0])) """The enemy made his turn, and now self.my_turn should changed to true and so on the current_player indicator in the gui""" self.my_turn = not self.my_turn self.current_player_var.set('Current player: ' + "Your Turn") if self.ai_on and self.game.game_over != self.game.YES: self.make_ai_move() # draw the board again after all the changes has been made self.draw() def draw(self): """Draw all the board with the disks and there's color""" """The two for loop runs on all the game.board and create disks according to the situation in the board""" for c in range(self.game.cols, -1, -1): """changes the board direction so the disks would be at the bottom and not at the top""" self.game.board = self.game.board[::-1] for r in range(self.game.rows, -1, -1): if r >= self.game.cols: continue x0 = c * self.ELEMENT_SIZE y0 = r * self.ELEMENT_SIZE x1 = (c + 1) * self.ELEMENT_SIZE y1 = (r + 1) * self.ELEMENT_SIZE """Create each disk according to the game board, if at the location the board is empty, then create disk with the default color - white, else - create the disk red/blue if the board at that location is red/blue """ if self.game.board[r][c] == self.game.BLUE: fill = self.PLAYER_1_COLOR elif self.game.board[r][c] == self.game.RED: fill = self.PLAYER_2_COLOR elif self.game.board[r][c] == self.game.RED + self.game.RED: fill = self.RED_WIN_COLOR elif self.game.board[r][c] == self.game.BLUE + self.game.BLUE: fill = self.BLUE_WIN_COLOR else: fill = self.DEFAULT_COLOR self.canvas.create_oval(x0 + 2, self.canvas.winfo_height() - (y0 + 2), x1 - 2, self.canvas.winfo_height() - (y1 - 2), fill=fill, outline=self.GRID_COLOR) self.game.board = self.game.board[::-1] if self.game.game_over == self.game.YES: """If the game is over, checks who won/lose/draw and print to the canvas message that says that""" text_width = text_height = self.canvas.winfo_height() / 2 # giving default win msg win_msg = "" # checks if there was a draw if self.game.get_winner() == self.game.DRAW: win_msg = "There Was a :" + "\n" + "Draw" """if when the game is over, the gui current var is "your turn" that mean the you lost the game """ elif self.current_player_var.get() == "Current player: Your Turn": win_msg = "You Lost" + "\n" + "The Game" elif self.current_player_var.get() ==\ "Current player: The Enemy Turn": win_msg = "You Won" + "\n" + "The Game" self.canvas.create_text(text_height, text_width, text=win_msg, font=("Helvetica", 32), fill="black") def draw_grid(self): """Draw the grid""" x0, x1 = 0, self.canvas.winfo_width() for r in range(1, self.game.rows): y = r * self.ELEMENT_SIZE self.canvas.create_line(x0, y, x1, y, fill=self.GRID_COLOR) y0, y1 = 0, self.canvas.winfo_height() for c in range(1, self.game.cols + 1): x = c * self.ELEMENT_SIZE self.canvas.create_line(x, y0, x, y1, fill=self.GRID_COLOR) def drop(self, column): """Make the move with on the game board with the given column and send a message to the opponent about the move that he just made, so the opponent gui board would be updated and update the current turn in the gui""" if self.game.board[0][column] == self.game.EMPTY: self.current_player_var.set('Current player: ' + "The Enemy Turn") self.__communicator.send_message(str(column)) return self.game.make_move(column) else: return def new_game(self): """Create the new game""" self.game = Game() self.canvas.delete(ALL) self.canvas.config(width=self.ELEMENT_SIZE * self.game.rows, height=self.ELEMENT_SIZE * self.game.cols) self.root.update() self.draw_grid() self.draw() def _action_when_canvas_clicked(self, event): """This function responsible for the action that happens, when the user click the board (the canvas)""" # do something only if its my_turn if self.my_turn: if self.game.game_over: # when the game is over, click would do nothing return c = event.x // self.ELEMENT_SIZE """if it the client/server my_turn is true, and the client/server clicked on a column then put the disk in that column""" if 0 <= c < self.game.rows: if not self.ai_on: self.drop(c) """After the client/server did his turn, his turn expired and changed to false, because after he did his move, the enemy move has come""" self.my_turn = not self.my_turn # draw the board after the move has been made self.draw() return def make_ai_move(self): """Do the ai move """ if self.game.game_over == self.game.YES: return column = self.ai.find_legal_move(self.game, self.game.make_move) self.__communicator.send_message(str(column)) # update the turn on display (on the GUI) self.current_player_var.set('Current player: ' + "The Enemy Turn") self.draw()
class Game: """ This class represents the game 4 in a row we were asked to implement. """ PLAYER_ONE = 0 PLAYER_TWO = 1 DRAW = 2 NUMBER_TO_PLAYER = {PLAYER_ONE: "Server", PLAYER_TWO: "Client"} CONTINUE_GAME = 3 ILLEGAL_MOVE = "Illegal move" PLAYER_ONE_WON_MSG = NUMBER_TO_PLAYER[PLAYER_ONE] + " has won the game" PLAYER_TWO_WON_MSG = NUMBER_TO_PLAYER[PLAYER_TWO] + " has won the game" DRAW_MSG = "There is a draw" DEFAULT_NUMBER_OF_ROWS = 6 DEFAULT_NUMBER_OF_COLS = 7 NUMBER_OF_ELEMENTS_IN_SEQUENCE = 4 WIN_NOT_FOUND = -2 DEFAULT_IP = socket.gethostbyname(socket.gethostname()) AI_DEFAULT_VALUE = None def __init__(self, is_human, is_server, port, ip_of_server=DEFAULT_IP): """ :param is_human: string. Describes if the player is human or ai. :param is_server: boolean value. If True, the user is the server and plays first, otherwise the player is the client and plays second. :param port: the port number the players uses for the game. goes between 1 to 65355. :param ip_of_server: the ip number of the server. the client insert this number. """ # Creates the board game self.board = Game_Board(Game.DEFAULT_NUMBER_OF_COLS, Game.DEFAULT_NUMBER_OF_ROWS, Game.PLAYER_ONE, Game.PLAYER_TWO, Game.DRAW, Game.WIN_NOT_FOUND, Game.NUMBER_OF_ELEMENTS_IN_SEQUENCE) self.root = tki.Tk() screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() # Creates the GUI method of the game self.gameGui = FourInARowGui(self.root, self.make_move, Game.DEFAULT_NUMBER_OF_ROWS, Game.DEFAULT_NUMBER_OF_COLS, screen_height, screen_width) # Defines which player begins. if is_server: self.root.title("Server") self.__communicator = Communicator(self.root, port) else: self.root.title("Client") self.__communicator = Communicator(self.root, port, ip_of_server) self.gameGui.lock_player() self.__communicator.bind_action_to_message(self.act_upon_message) self.__communicator.connect() # sets whose turn it is current_player = self.get_current_player() self.change_whose_turn_label(self.get_other_player(current_player)) # Creates AI user if needed self.ai = Game.AI_DEFAULT_VALUE if not is_human: self.ai = AI() if is_server: self.make_ai_move() else: self.gameGui.lock_player() self.run_game() def run_game(self): """ The function runs the game. """ self.root.mainloop() def make_ai_move(self): """ Makes an AI move if there is an ai player. :return: """ col = self.ai.find_legal_move(self, self.make_ai_func()) self.gameGui.simulate_press_by_ai(col) def act_upon_message(self, message): """ :param message: a message given by the client or the server the message is expected to be the number of the column that was pressed :return: """ current_player = self.get_current_player() self.change_whose_turn_label(current_player) self.gameGui.unlock_player() self.gameGui.simulate_press(int(message)) if self.ai is not Game.AI_DEFAULT_VALUE: self.make_ai_move() def send_action_to_other_player(self, column): """ :param column: the column the user has pressed locks the current player from pressing any other column and sends the column that was pressed on as a message to the other player """ current_player = self.get_current_player() self.change_whose_turn_label(current_player) self.gameGui.lock_player() self.__communicator.send_message(str(column)) def change_whose_turn_label(self, current_player): """ :param current_player: the code of the current player changes the label such that it will say that the turn is that of the other player """ whose_turn = \ Game.NUMBER_TO_PLAYER[self.get_other_player(current_player)] + " Turn" self.gameGui.change_top_label(whose_turn) def get_other_player(self, current_player): """ :param current_player: the player who is playing now :return: the other player """ if current_player == Game.PLAYER_ONE: return Game.PLAYER_TWO if current_player == Game.PLAYER_TWO: return Game.PLAYER_ONE def make_move(self, column, me_has_pressing=True, illegal_move_was_made=False): """ This function implement single move in the game :param column: the column the player choose :param me_has_pressing: if the player pressed something :param illegal_move_was_made: a boolean, if its true an exception will be raised """ if illegal_move_was_made: raise Exception(Game.ILLEGAL_MOVE) if me_has_pressing: self.send_action_to_other_player(column) current_player = self.get_current_player() if not self.board.valid_player(current_player): raise Exception(Game.ILLEGAL_MOVE) else: self.board.move_of_player(current_player, column) self.check_status() def check_status(self): """ The function deals with cases of winning or draw in the game. Than it shuts the game down. """ win_check = self.board.check_if_there_is_win() if win_check: # The winner player and the sequence of slots that won the game winner = win_check[0] win_sequence = win_check[1] win_sequence = Game.reverse_tuples_in_list(win_sequence) # Displaying the message to the players self.gameGui.game_over(self.final_stage_msg(winner), win_sequence) self.reset_ai() if self.board.check_board_if_full(): # Displaying draw message to the players self.gameGui.game_over(self.final_stage_msg(Game.DRAW)) self.reset_ai() def get_winner(self): """ this function checks if there is a winner or a draw if there is a winner it returns the code of the player that won if there is a draw it returns the code for a draw if none of the above it returns None """ win_check = self.board.check_if_there_is_win() if win_check: winner = win_check[0] return winner if self.board.check_board_if_full(): return Game.DRAW def reset_ai(self): """ Changes the ai to None """ self.ai = Game.AI_DEFAULT_VALUE def final_stage_msg(self, winner_num): """ printing message to the screen for win or draw """ if winner_num == Game.PLAYER_ONE: return Game.PLAYER_ONE_WON_MSG if winner_num == Game.PLAYER_TWO: return Game.PLAYER_TWO_WON_MSG if winner_num == Game.DRAW: return Game.DRAW_MSG def get_player_at(self, row, col): """ returns which player is on specific location """ board = self.board.get_board() return board[row][col] def get_current_player(self): """ returns whose turn is it now """ return self.board.board_status() def make_ai_func(self): """ Implement the moves of the AI. """ def make_ai_move(col): self.gameGui.simulate_press(col) return make_ai_move @staticmethod def reverse_tuples_in_list(list_of_tuples): """ :param list_of_tuples returns the same list with the same tuples but repositioned. """ new_list = [] for tpl in list_of_tuples: new_list.append(tuple(reversed(tpl))) return new_list
class Game: """ Game class, handles gameplay, gui and communicator """ PLAYER_ONE = 0 PLAYER_TWO = 1 DRAW = 2 EMPTY = 3 MIN_PORT_VALUE = 1000 MAX_PORT_VALUE = 65535 WINNING_SEQ_LENGTH = 4 ILLEGAL_MOVE = "Illegal move" INVALID_MESSAGE = "Invalid message received" INVALID_GAME_STATE = "Invalid game state" CHECK_WINNER_FLAG_INDEX = 29 ADD_CHIP_FAILED = "Failed to add chip to specified column" COMMUNICATOR_MESSAGE_1 = "CHIP_DATA: " COMMUNICATOR_MESSAGE_2 = "CHECK_WINNER: " EXPECTED_AMOUNT_OF_ARGUMENTS_FOR_CLIENT = 4 EXPECTED_LENGTH_OF_MESSAGE = 30 def __init__(self): """ Initializes the game class """ # If server mode is on (no IP address provided) if len(argv) == 3: self.__player = self.PLAYER_ONE self.__enemy_player = self.PLAYER_TWO self.__current_player = self.__player else: self.__player = self.PLAYER_TWO self.__enemy_player = self.PLAYER_ONE self.__current_player = self.__enemy_player # Create board self.__board = Board(self.get_current_player) self.__game_over = False # initializes AI if AI was chosen at run self.__ai_flag = False if argv[1] == "ai": self.__ai = AI() self.__ai_flag = True # Create gui self.__gui = GUI(self.__player, self.__make_move, self.get_current_player, self.__ai_flag) # If this is the client, disable buttons until player turn if self.__player == self.PLAYER_TWO: self.__gui.disable_column_buttons() # TODO:: Ugly code, find a workaround self.__last_inserted_chip = None # Parse data for communicator port = int(argv[2]) ip = None if len(argv) == self.EXPECTED_AMOUNT_OF_ARGUMENTS_FOR_CLIENT: ip = argv[3] # Initializes communicator self.__communicator = Communicator(self.__gui.get_root(), port, ip) self.__communicator.connect() self.__communicator.bind_action_to_message(self.parse_rival_message) # If AI on server start the game, make a move if self.__ai_flag and self.__player == self.PLAYER_ONE: self.__ai.find_legal_move(self, self.__make_move) # Start the gui and game self.__gui.get_root().mainloop() def __make_move(self, column): """ :param column: Column in whivh to place chip """ # if game over flag on, returns if self.__game_over: return # attempts to place chip in column success, row = self.__board.check_legal_move_get_row( column, self.PLAYER_ONE if not self.__current_player else self.PLAYER_TWO) if not success: raise Exception(self.ILLEGAL_MOVE) # Store move for other functions self.__last_inserted_chip = column, row # Relay move to enemy self.__communicator.send_message(self.COMMUNICATOR_MESSAGE_1 + str(column) + "," + str(row) + " " + self.COMMUNICATOR_MESSAGE_2 + "1" if not self.__game_over else "0") self.__check_winner(column, row) def __check_winner(self, column, row): """ :param column: Column of newest chip :param row: Row of newest chip """ # Get data if a winning state was reached winner, winning_chips = self.__board.find_connected_and_winner( column, row) # Get pixel location for newest chip x, y = self.__board.get_chip_location(column, row) if winner is None: # If game is still ongoing # Create the chip on board self.__gui.create_chip_on_board(x, y, self.__current_player, board=self.__board) # Toggle __player in class members self.__toggle_player() # Disable full columns self.__gui.disable_illegal_columns(self.__board) else: # Game ended self.__game_over = True if winner == self.DRAW: self.__gui.create_chip_on_board(x, y, self.__current_player, board=self.__board) self.__gui.disable_column_buttons() self.__gui.show_game_over_label(self.DRAW) else: self.__gui.create_chip_on_board(x, y, self.__current_player, winning_chips=winning_chips, board=self.__board, winner=winner) def __toggle_player(self): """ Toggles members in the class, also make gui show switching of turns """ self.__current_player = self.PLAYER_TWO \ if self.__current_player == self.PLAYER_ONE \ else self.PLAYER_ONE flag = self.__current_player == self.__player self.__gui.end_turn_switch_player(flag) def get_winner(self): """ Gets the winner if there is one. This function is not used by the game """ return self.__board.find_connected_and_winner( self.__last_inserted_chip[0], self.__last_inserted_chip[1])[0] def get_player_at(self, row, col): """ :param row: Row to check :param col: Column to check :return: Player at place """ player = int(self.__board.get_columns()[col][row]) return None if player == self.EMPTY else player def get_current_player(self): """ Getter for current __player """ return self.__current_player def get_board(self): """ Getter for board """ return self.__board def parse_rival_message(self, message): """ :param message: Message received from enemy """ # Check message of corect length if len(message) != self.EXPECTED_LENGTH_OF_MESSAGE: raise Exception(self.INVALID_MESSAGE) # Parse data from message column = int(message[11]) expected_row = int(message[13]) # Update board and check if same row was returned success, row = self.__board.check_legal_move_get_row( column, self.PLAYER_ONE if not self.__current_player else self.PLAYER_TWO) # Assert it assert row == expected_row if success: # Update member self.__last_inserted_chip = column, row check_winner_flag = message[self.CHECK_WINNER_FLAG_INDEX] if check_winner_flag: self.__check_winner(column, row) else: raise Exception(self.ADD_CHIP_FAILED) self.__gui.disable_illegal_columns(self.__board) # If the AI is playing, make another move if self.__ai_flag and not self.__game_over: self.__ai.find_legal_move(self, self.__make_move) def get_last_inserted_chip(self): """ Getter for last inserted chip """ return self.__last_inserted_chip
class GUI: """ A class responsible for handling the GUI aspects of a for in a row game. Also handles the connection of the communicator """ IMAGE_PATHS = [ "images/empty_cell.png", "images/player1_disk.png", "images/player2_disk.png", "images/player1_disk_win.png", "images/player2_disk_win.png" ] FONT = ("times", 24) HUMAN = 0 AI = 1 MSG_COLOR = "blue" EMPTY_CELL_IMG = 0 PLAYER1_CELL_IMG = 1 PLAYER2_CELL_IMG = 2 PLAYER1_WIN_DISK = 3 PLAYER2_WIN_DISK = 4 WINDOW_SIZE = 500 BOARD_HEIGHT = 6 BOARD_WIDTH = 7 P1 = "Player 1" P2 = "Player 2" HOLD_MSG = "Please wait for %s to finnish their turn" DRAW = "No more moves. Game ended with a draw" WIN = "%s won!" DEFAULT_MSG = "" TOP = 0 DROP_RATE = 100 def __init__(self, root, player, port, ip=None): """ Initialize GUI and connect the communicator :param root: The tkinter root :param player: An integer representing if the player is human or ai :param port: The port to connect to :param ip: The ip to connect to """ self.__root = root self.__game = Game() self.__bottoms = self.__game.get_board().get_bottoms() self.__communicator = Communicator(root, port, ip) self.__communicator.connect() self.__communicator.bind_action_to_message(self.__handle_message) self.__top_frame = tk.Frame(self.__root) self.__top_frame.pack() self.__col_frames = [] self.__buttons = [] self.__player = GUI.P1 self.__opponent = GUI.P2 if ip is not None: self.__player, self.__opponent = self.__opponent, self.__player self.__msg_board = tk.Message(root, font=GUI.FONT, fg=GUI.MSG_COLOR) self.__msg_board.pack() self.__images = [] self.__load_images() self.__place_widgets() self.__ai = None if player == GUI.AI: self.__ai = AI() self.__start_game() def __start_game(self): """ Allow player one to make a move :return: None """ if self.__player == GUI.P1: if self.__ai is not None: self.__ai_play() else: self.__change_buttons_state(tk.NORMAL) else: self.__update_msg(GUI.HOLD_MSG % self.__opponent) def __place_widgets(self): """ Place widgets in the gui :return: None """ for col in range(GUI.BOARD_WIDTH): col_frame = tk.Frame(self.__top_frame) col_frame.pack(side=tk.LEFT) self.__col_frames.append(col_frame) for row in range(GUI.BOARD_HEIGHT): cell = tk.Label(col_frame, image=self.__images[GUI.EMPTY_CELL_IMG]) cell.grid(row=row) col_button = tk.Button( col_frame, text=str(col + 1), command=lambda column=col: self.__play_col(column), state=tk.DISABLED) col_button.grid(row=GUI.BOARD_HEIGHT) self.__buttons.append(col_button) def __load_images(self): """ Load all images necessary for the gui :return: None """ images = [] for path in GUI.IMAGE_PATHS: self.__images.append(tk.PhotoImage(file=path)) return images def __play_col(self, col, report=True): """ Make a move in a column :param col: The column's index :return: None """ self.__update_msg(GUI.DEFAULT_MSG) self.__change_buttons_state(tk.DISABLED) player = self.__game.get_current_player() frame = self.__col_frames[col] if self.__game.get_current_player() == Game.PLAYER_ONE: new_img = self.__images[GUI.PLAYER1_CELL_IMG] else: new_img = self.__images[GUI.PLAYER2_CELL_IMG] self.__game.make_move(col) bottom = self.__bottoms[col] + 1 if bottom == GUI.TOP: self.__buttons[col] = None for cell_index in range(bottom): cell = frame.grid_slaves(row=cell_index)[0] self.__disk_through_cell(cell, player) last_cell = frame.grid_slaves(row=bottom)[0] last_cell.configure(image=new_img) winner = self.__game.get_winner() if report: self.__communicator.send_message(col) if winner is None: self.__update_msg(GUI.HOLD_MSG % self.__opponent) if winner is not None: self.__handle_win(winner) def __update_msg(self, msg): """ Update the message widget with a new message :param msg: A string :return: None """ self.__msg_board.configure(text=msg) def __handle_win(self, winner): """ Change the board to fit the end state of the game :param winner: An integer representing the winner of the game :return: None """ self.__change_buttons_state(tk.DISABLED) self.__buttons = [] if winner == Game.DRAW: self.__update_msg(GUI.DRAW) return elif winner == Game.PLAYER_ONE: msg = GUI.WIN % GUI.P1 img = self.__images[GUI.PLAYER1_WIN_DISK] else: msg = GUI.WIN % GUI.P2 img = self.__images[GUI.PLAYER2_WIN_DISK] for cell in self.__game.get_wining_streak(): row, col = cell frame = self.__col_frames[col] cell_widget = frame.grid_slaves(row=row)[0] cell_widget.configure(image=img) self.__update_msg(msg) def __handle_message(self, move): """ Specifies the event handler for the message getting event in the communicator. Makes a move when invoked :param move: A string representing a single move in the game :return: None """ move = int(move) self.__play_col(move, report=False) if self.__ai is not None: self.__ai_play() else: self.__change_buttons_state(tk.NORMAL) def __ai_play(self): """ Make a play with the ai :return: None """ if self.__game.get_winner() is None: self.__ai.find_legal_move(self.__game, self.__play_col) def __change_buttons_state(self, state): """ Change all relevant buttons' state :param state: Either normal or disabled :return: None """ for button in self.__buttons: try: button.configure(state=state) except AttributeError: continue def __disk_through_cell(self, cell, player=None): """ Animate a disk going through an empty cell :param cell: The cell :param player: The player whose disk is dropping :return: None """ if player is not None: if player == Game.PLAYER_ONE: cell.configure(image=self.__images[GUI.PLAYER1_CELL_IMG]) else: cell.configure(image=self.__images[GUI.PLAYER2_CELL_IMG]) cell.update() cell.after(GUI.DROP_RATE, self.__disk_through_cell(cell)) else: cell.configure(image=self.__images[GUI.EMPTY_CELL_IMG])
class FourInARow: """Class representing a game of connect-4 with graphics""" def __init__(self, parent, player, port, ip=None): """Instructor of FourInARow object""" self._end_game = False self.__init_graphics__() self._game = Game() self._root = parent self._status = None self._player = player self.__init__ai_or_human() self.__init__ip_distinguisher(ip) self.__communicator = Communicator(self._root, port, ip) self.__communicator.connect() self.__communicator.bind_action_to_message(self.__handle_message) def __init_graphics__(self): """initiates the graphic of the game""" self._player1_disc = PhotoImage(file=PLAYER_ONE_DISK) self._player2_disc = PhotoImage(file=PLAYER_TWO_DISK) self._player1_turn = PhotoImage(file=PLAYER_ONE_TURN) self._player2_turn = PhotoImage(file=PLAYER_TWO_TURN) self._win_disc = PhotoImage(file=WIN_DISC) self._player1_won = PhotoImage(file=PLAYER_ONE_WIN_SCREEN) self._player2_won = PhotoImage(file=PLAYER_TWO_WIN_SCREEN) self._draw_screen = PhotoImage(file=DRAW_SCREEN) def __init__ai_or_human(self): """initiates the type of player""" if self._player == HUMAN_PLAYER: self.__init__new_canvas(BOARD) self._canvas.bind("<Button-1>", self.game_screen_callback) if self._player == AI_PLAYER: self.__init__new_canvas(BOARD) self._ai = AI() self._root.after(1, self.make_ai_move) def __init__ip_distinguisher(self, ip): """initiates the player num whether ip is None or not""" if ip is not None: self._player_num = self._game.PLAYER_TWO else: self._player_num = self._game.PLAYER_ONE def __init__new_canvas(self, img): """this method receives an image initiates a new canvas with it.""" self._background = PhotoImage(file=img) self._canvas = Canvas(self._root, height=BACKGROUND_HEIGHT, width=BACKGROUND_WIDTH) self._canvas.create_image(3, 3, image=self._background, anchor=NW) self._canvas.pack() def make_ai_move(self): """makes a move for an ai player""" if not self._end_game: if self._game.get_current_player() == self._player_num: col = self._ai.find_legal_move(self._game, self.general_move) self.__communicator.send_message(str(col)) self._root.after(1, self.make_ai_move) def __handle_message(self, text=None): """this method receives text that represent a column-index and operates general_move with this column.""" if text: column = int(text) self.general_move(column) def game_screen_callback(self, event): """the callback method for the game-screen. The method receives an event and operates other func whether the event was under certain conditions or not""" # numbers in this function represent coordinates on the screen only! # if self._game.get_current_player() != self._player_num: # return x = event.x y = event.y for col in range(game.COLUMNS): if x in range(39 + col * 63, 86 + col * 63) and 26 < y < 447: if self.general_move(col): self.__communicator.send_message(str(col)) def general_move(self, column): """this is the general method for making moves in the game. It receives a column-index and inserts the current player's disc in this column (in the game's board and in the graphic screen as well). If something went wrong during the process an exception is raised.""" self._game.make_move(column) row_counter = 0 for i in range(len(self._game.get_board()) - 1, -1, -1): if self._game.get_board()[i][column] == game.board.FREE_SPACE: break row_counter += 1 self.add_disc(column, game.ROWS - row_counter) return True def add_disc(self, col, row): """adds a current player's graphic disc to the screen.""" # numbers in this function represent coordinates on the screen only! if self._game.get_current_player() == Game.PLAYER_ONE: self._canvas.create_image(64 + 64 * col, 70 + 56.5 * row, image=self._player1_disc) self._canvas.create_image(559, 211, image=self._player1_turn) else: self._canvas.create_image(64 + 64 * col, 70 + 56.5 * row, image=self._player2_disc) self._canvas.create_image(559, 211, image=self._player2_turn) self.game_status() def game_status(self): """checks for the game status. Whether one of the players won or its a draw, and operates other methods according to the status.""" self._status = self._game.get_winner() if self._status[0] in [self._game.PLAYER_ONE, self._game.PLAYER_TWO]: self.show_winner(self._status[0], self._status[1]) self._canvas.bind("<Button-1>", self.exit_game) self._end_game = True if self._status[0] == self._game.DRAW: self._canvas.create_image(3, 3, image=self._draw_screen, anchor=NW) self._canvas.bind("<Button-1>", self.exit_game) self._end_game = True def show_winner(self, winner, win_discs_list): """if a winner was found in the game status method, this method show's the winner's discs that made a sequence and the winner player himself.""" # numbers in this function represent coordinates on the screen only! for disc in win_discs_list: row, col = disc self._canvas.create_image(64 + 64 * col, 70 + 56.5 * row, image=self._win_disc) if winner == self._game.PLAYER_ONE: self._canvas.create_image(3, 3, image=self._player1_won, anchor=NW) else: self._canvas.create_image(3, 3, image=self._player2_won, anchor=NW) def exit_game(self, event): """this method ends the game (including graphics).""" if event: self._root.quit() self._root.destroy()
class Gui: """ This class is in charge of the graphics of the game, and operating it as well. It changes the graphic board accordingly to the changes in the Game object (a board dictionary). """ TIE_SCORE = "It's a tie!" OPEN_MESSAGE = 'WELCOME!' WINNER_MESAGGE = 'YOU WON!' LOSER_MESSAGE = 'YOU LOST!' COL_0_COORD = 77 COL_FACTOR = 90 ROW_0_COORD = 557 ROW_FACTOR = 67 INDICATOR = 'column' TAG = 'player' PLAY = 'Your turn' DONT_PLAY = "Oppenent's turn" ON = 1 OFF = 0 INDICATOR_ROW = -1 CANVAS_WID = 700 CANVAS_HIGHT = 600 ILLEGAL_TAG = 'illegal' OUT_OF_BOARD = 50 TIME_OF_ILLEGAL_MOVE = 250 WELCOME_FONT_SIZE = 50 MAIN_TITLE_FONT_SIZE = 50 SECOND_TITLE_FONT_SIZE = 30 CENTER_WIDTH = 350 PINEAPPLE_IMG = 'pineapple.png' COCONUT_IMG = 'coconut1.png' BACKGROUND_IMG = 'background.png' WINNER_IMG = 'like.png' ILLEGAL_IMG = 'illegal_move.png' COL_START = 31 FACTOR = 91 def __init__(self, root, ip, port, server, is_human): """ the game starts with a board (from class Game), creates canvas with all the images needed, and starts a communicator object :param root: the Tk 'parent' object :param ip: servers ip :param port: the game port :param server: True or False """ self.__board = Game() self.__root = root self.__is_human = is_human self.__coconut_img, self.__pineapple_img, self.__background_img, \ self.__winner_img, self.__illegal_img = self.create_images() self.__col_lst = [] # used for the indicator self.__indicator = self.OFF # a 'switch' for the indicator self.__illegal_sign = self.OFF # counter for illegal move sign self.__canvas, self.__title, self.__second_title = self.create_canvas( self.__background_img) self.__communicator = Communicator(root, port, ip) self.__communicator.connect() self.__communicator.bind_action_to_message(self.__handle_message) self.__is_playing = server if self.__is_human == COMPUTER: self.__ai = AI() self.initialization() def initialization(self): """ initials the players. for human player - activates his mouse, for ai player - starting to play """ if player_type == COMPUTER and is_server: self.play_with_comp() if player_type == HUMAN: self.__canvas.bind("<Button-1>", self.callback) self.__canvas.bind("<Motion>", self.motion) def create_images(self): """ creates the images needed for the graphic display :return: the images """ coconut_img = tk.PhotoImage(file=self.COCONUT_IMG) pineapple_img = tk.PhotoImage(file=self.PINEAPPLE_IMG) background_img = tk.PhotoImage(file=self.BACKGROUND_IMG) winner_img = tk.PhotoImage(file=self.WINNER_IMG) illegal_img = tk.PhotoImage(file=self.ILLEGAL_IMG) return coconut_img, pineapple_img, background_img, winner_img, \ illegal_img def create_canvas(self, background_img): """ creates the canvas that will be the graphic game board :param background_img: the background image for the canvas :return: the canvas, the board title and a secondary title to be used later """ canvas = tk.Canvas(self.__root, width=self.CANVAS_WID, height=self.CANVAS_HIGHT, background='white') canvas.create_image(self.CENTER_WIDTH, 300, image=background_img, anchor=tk.CENTER) if self.__is_human == HUMAN: title = canvas.create_text(self.CENTER_WIDTH, 50, text=self.OPEN_MESSAGE, font=('', self.WELCOME_FONT_SIZE), fill='white') if is_server: # server starts the game.his title would be # accordingly second_title = canvas.create_text( self.CENTER_WIDTH, 100, text=self.PLAY, font=('', self.SECOND_TITLE_FONT_SIZE), fill='white') else: second_title = canvas.create_text( self.CENTER_WIDTH, 150, text=self.DONT_PLAY, font=('', self.SECOND_TITLE_FONT_SIZE), fill='white') canvas.pack() return canvas, title, second_title else: empty_title = canvas.create_text(350, 100, text='', font=('', 30), fill='white') canvas.pack() return canvas, empty_title, empty_title def motion(self, event): """ shows the current player what is the column his mouse is pointing on :param event: the location of the mouse (x and y coordinates) """ if self.__is_playing: self.__col_lst.append(self.get_column(event.x)) column = self.get_column(event.x) if column is None: return # Checking who is playing: player = self.__board.get_current_player() if self.__indicator == self.OFF: # no indicator on screen # the screen self.put_image_helper(self.INDICATOR_ROW, column, player, self.INDICATOR) self.__indicator = self.ON if self.__col_lst[0] == self.__col_lst[-1]: # first and last # item are the same- mouse is still on the same column return self.delete_img(self.INDICATOR) self.__col_lst = [] self.__indicator = self.OFF def delete_img(self, name): """ deleting an image from screen with a given name (tag). """ tag = self.__canvas.find_withtag(name) if tag: # if the image exist on the board at the moment self.__canvas.delete(tag) def __handle_message(self, column): """ when receiving a message it means that one player had played and now it's the second one turn. this function is placing the disc that the first player put in his turn on the second's player board. if the second player is an ai player- it makes a move. :param column: the column the user clicked. """ self.put_image(int(column)) if self.__is_human == COMPUTER and self.__board.get_winner() is None: self.play_with_comp() # calling to computer to act def get_column(self, event_x): """ finds the column that fits the dictionary board coordinates form, according to the pixel the user clicked on :param event_x: the x-coordinate of the user click :return: the column in boards coordinate (int in range(7)) """ if event_x in range(self.COL_START, self.COL_START + self.COL_FACTOR): # range 31,122 return 0 elif event_x in range(self.COL_START + self.FACTOR, self.COL_START + 2 * self.FACTOR): # range 122,213 return 1 elif event_x in range(self.COL_START + 2 * self.FACTOR, self.COL_START + 3 * self.FACTOR): # range 213,304 return 2 elif event_x in range(self.COL_START + 3 * self.FACTOR, self.COL_START + 4 * self.FACTOR): # range 304,395 return 3 elif event_x in range(self.COL_START + 4 * self.FACTOR, self.COL_START + 5 * self.FACTOR): # range 395,486 return 4 elif event_x in range(self.COL_START + 5 * self.FACTOR, self.COL_START + 6 * self.FACTOR): # range 486,577 return 5 elif event_x in range(self.COL_START + 6 * self.FACTOR, self.COL_START + 7 * self.FACTOR): # range 577,668 return 6 else: return def put_image_helper(self, row, column, player, tag): """ puts the user disc in its place according to a given 'row' and 'column'. gets the current player, to determine which disc should be placed there. 'tag' is used for image that we want to remove later on. :param row: a given row number :param column: a given row number :param player: current player :param tag: the tag of the image (a string) :return: """ if player == Game.PLAYER_ONE: self.__canvas.create_image( self.COL_0_COORD + column * self.COL_FACTOR, self.ROW_0_COORD - (5 - row) * self.ROW_FACTOR, image=self.__coconut_img, tag=tag) elif player == Game.PLAYER_TWO: self.__canvas.create_image( self.COL_0_COORD + column * self.COL_FACTOR, self.ROW_0_COORD - (5 - row) * self.ROW_FACTOR, image=self.__pineapple_img, tag=tag) def put_image(self, column): """ gets the column (integer number in range(7)), and checking if its possible to put disc in this column. if it is- it will update the Game() object and update the graphic board by calling 'put_image_helper'. after putting the disc, the is_plying will change to True, it means that the other player can play now. :param column: the column the user chose. :return: True if the disc was added. """ if self.__board.legal_assignment(column): # if the user move was legal player = self.__board.get_current_player() row = self.__board.make_move(column) self.put_image_helper(row, column, player, self.TAG) if self.__is_human == HUMAN: self.__canvas.itemconfig(self.__title, text=self.PLAY, font=('', self.MAIN_TITLE_FONT_SIZE)) self.__canvas.delete(self.__second_title) self.__is_playing = True self.get_game_status() # checks if the game is over or continues return True else: self.__canvas.create_image(self.CENTER_WIDTH, 300, image=self.__illegal_img, tag=self.ILLEGAL_TAG) self.__canvas.after(self.TIME_OF_ILLEGAL_MOVE, lambda: self.__canvas.delete(self.ILLEGAL_TAG)) def paint_winner(self): """ checks if there is a winner. if there is- it marks the 4 winning discs """ win_coords = self.__board.get_win_seq() for coord in win_coords: row = coord[0] col = coord[1] self.__canvas.create_image( self.COL_0_COORD + col * self.COL_FACTOR, self.ROW_0_COORD - (5 - row) * self.ROW_FACTOR, image=self.__winner_img) def callback(self, event): """ when user clicks on the board, the board should update. this function will react only if self.__is_playing=True, otherwise, clicking on the board will do nothing. its sends a message with the column clicked to the other player. :param event: x and y coordinates of the user click """ if self.__is_playing: # user clicks outside the top border of the board - do nothing if event.y < self.OUT_OF_BOARD: return column = self.get_column(event.x) if column is None: return if self.put_image(column): self.delete_img(self.INDICATOR) self.__indicator = self.OFF # no indicator on screen self.__canvas.delete(self.__second_title) self.__canvas.itemconfig(self.__title, text=self.DONT_PLAY, font=('', self.MAIN_TITLE_FONT_SIZE)) # after a player had made a move, disable his mouse self.__is_playing = False # send message to the other player, so he can play: self.__communicator.send_message(column) self.get_game_status() # checks if the game is over or continues def get_game_status(self): """ checks if there is a winner, or draw in the game """ if self.__board.get_winner() is not None: # there is a winner self.__canvas.unbind("<Button-1>") # human can't use their mouse self.__canvas.unbind("<Motion>") winner = self.__board.get_winner() if winner is self.__board.DRAW: self.__canvas.itemconfig(self.__title, text=self.TIE_SCORE, font=('', self.MAIN_TITLE_FONT_SIZE)) else: if (is_server and not winner) or (not is_server and winner): # display to each player if he won or lost the game self.__canvas.itemconfig(self.__title, text=self.WINNER_MESAGGE, font=('', self.MAIN_TITLE_FONT_SIZE)) else: self.__canvas.itemconfig(self.__title, text=self.LOSER_MESSAGE, font=('', self.MAIN_TITLE_FONT_SIZE)) self.paint_winner() # displaying the winning discs def play_with_comp(self): """ when this function is called, computer is playing. the function determine in what column the computer should put the disc (if its possible), then makes the move, and sends a message to the other player with the column the disc was placed in. :return: """ col = self.__ai.find_legal_move(self.__board, self.__board.make_move) self.put_comp_image(col) # send message to the other player, so he can play: self.__communicator.send_message(col) self.get_game_status() def put_comp_image(self, col): """ checking which disc player should be added to the board, than check what is the row that the disc will be placed in, than call 'put_image_helper' and update the graphic board. (Game() object has already updated). :param col: an integer (the column that the computer chose). :return: """ # disc is already updated on the board, so we need the opposite # player (not current one) num_of_discs = self.__board.count_discs() if num_of_discs % 2 == 0: player = self.__board.PLAYER_TWO else: player = self.__board.PLAYER_ONE row = self.__board.get_comp_row(col) self.put_image_helper(row, col, player, self.TAG) self.get_game_status()
class GUI: """ Designed to handle the GUI aspects (creating a window, buttons and pop-ups. Also initializes the communicator object, the game object and the AI object. """ SCALAR = 99 # Determines the screen size and the sizes of objects # within it. Any int or float will work. # **The dimensions of the screen must be kept at a 6 to 7 ratio for the # game to work correctly.** WIDTH = 7 * SCALAR # Width of the screen in pixels HEIGHT = 6 * SCALAR # Height of the screen in pixels def __init__(self, game, master, port, player=None, ip=None): """ Initializes the GUI and connects the communicator and the AI. :param game: The game class which includes the logic of the game. :param master: The tkinter root. :param port: The port to connect to. :param player: The player to start as. If server then player one, if client then player two. :param ip: The ip address of the server. to be left None if the instance is server. """ self._master = master # The tkinter root. self._canvas = t.Canvas(self._master, width=self.WIDTH, height=self.HEIGHT, highlightthickness=0) # The tkinter canvas. # The AI class which is to be activated only when the game is not # human vs. human. self._AI = AI() self._game = game # The communicator class which is in charge of communication between # server and client. self.__communicator = Communicator(root, port, ip) # Connects between instances of the game. self.__communicator.connect() # Binds the message handler to incoming messages. self.__communicator.bind_action_to_message(self.__handle_message) # __columns: A dictionary of keys: Column numbers. # values: The columns' item IDs. # __discs: A dictionary of keys: Coordinates on the board. # values: The Discs' item IDs. self.__columns, self.__discs = self.shapes() # if the instance is AI, is_human changes to False. self.__is_human = True self.__player = player def set_is_human(self, is_human): """ Sets the __is_human attribute to true if the player is human and false if the player is 'AI'. :param is_human: True if human or False if AI """ self.__is_human = is_human def event_handler(self, column, msg=False): """ The function to be activated when a move is made by one of the players. Calls all the necessary functions to be executed when a turn has been made. :param column: The column which was clicked by a player. :param msg: Determines whether the move was made by the current player or by the other player. :return: None. """ if msg: self._game.make_move(column) self.add_disc() self.win() self.tie() elif self.__player == self._game.get_current_player() and \ self._game.get_winner() == None: self._game.make_move(column) self.add_disc() self.__communicator.send_message(str(column)) self.win() self.tie() def add_disc(self): """ Adds a disc to the screen according to the coordinates supplied by the "make move" function. Add a green or red disc depending on who the current player is. :return: None. """ game = self._game y, x = game.get_last_move() item_id = self.__discs[x, y] if game.get_current_player() == game.PLAYER_ONE: self._canvas.itemconfig(item_id, fill="#af0707", outline='#751810') else: self._canvas.itemconfig(item_id, fill="#096300", outline='#203d11') def shapes(self): """ The function that draws the initial board. Generates all of the necessary shapes by using the helper functions. :return: A dictionary of columns and a dictionary of discs. """ self._canvas.pack() column_dict = {k: None for k in range(int(self.WIDTH / self.SCALAR))} disc_dict = self._game.create_board() for i in range(int(self.WIDTH / self.SCALAR)): # Creates columns. color = self.color_generator \ (index=i, RGB=(0, 0, 0, 0, 3, 0), oval=False) rectangle = self.draw_column(i, color) column_dict[i] = rectangle # adds to column dictionary. for j in range(int(self.HEIGHT / self.SCALAR)): # Creates discs. color = self.color_generator(index=(i, j), RGB=(1, 0, 0, 0, 2, 1), oval=True) oval = self.draw_oval(i, j, color) disc_dict[i, j] = oval # adds to disc dictionary. return column_dict, disc_dict def key_bind(self, object, index): """ Binds Left mouse button to event handler, as well as 'enter' and 'leave' to the current player signal (changing the column color). :param object: Type of object to bind. :index: Column index of the object. """ self._canvas.tag_bind(object, '<Button-1>', lambda event: self.event_handler(index, False)) self._canvas.tag_bind(object, '<Enter>', lambda event: self.column_config(index, True)) self._canvas.tag_bind(object, '<Leave>', lambda event: self.column_config(index, False)) def draw_oval(self, i, j, color): """ Creates The oval objects to be displayed on screen. :param i: Current column index. :param j: Current row index. :param color: The shade to fill the oval with. :return: Creates an oval object """ scaled_i, scaled_j = i * self.SCALAR, j * self.SCALAR tlo = self.SCALAR * 0.2 # top left offset bro = self.SCALAR * 0.8 # bottom right offset oval = self._canvas.create_oval(scaled_i + tlo, scaled_j + tlo, scaled_i + bro, scaled_j + bro, fill=color, outline='', width=2) self.key_bind(oval, i) return oval def draw_column(self, i, color): """ Used for the initial drawing of columns to the screen. Binds Left mouse button to event handler, as well as 'enter' and 'leave' to the current player signal (changing the column color). :param canvas: Canvas to draw on. :param i: Current column index. :param color: Fill color of the column. :return: Creates a column (rectangle object) on the screen. """ scaled_i = self.SCALAR * i tlo = 0 # top left offset rectangle = self._canvas.create_rectangle(scaled_i, tlo, scaled_i + self.SCALAR, self.HEIGHT, fill=color, outline='') self.key_bind(rectangle, i) return rectangle def column_config(self, i, enter): """ Changes the color of the column on mouse over depending on the current player. If it is not the instance's turn, the column will be grayed out to avoid confusion and still let the player know that the game is not stuck. :param i: Column index. :param enter: True if 'enter' event, False if 'leave' event. """ current_player = self._game.get_current_player() item_id = self.__columns[i] if enter and self.__player == current_player: if self.__player == self._game.PLAYER_ONE: self._canvas.itemconfig(item_id, fill='#720d0d') else: self._canvas.itemconfig(item_id, fill='#16720d') elif enter and self.__player != current_player: self._canvas.itemconfig(item_id, fill='#555555') elif not enter: color = self.color_generator(index=i, RGB=(0, 0, 0, 0, 3, 0), oval=False) self._canvas.itemconfig(item_id, fill=color) def color_generator(self, index, RGB, oval): """ A function that is used to generate the various shades that are utilized in the initial creation of the board. The formula for the coloring of the ovals was found solely through rigorous trial and error. This gives the illusion of a gradient background. :param index: When sent from oval, this is a tuple of the row and column. This enables the illusion of a gradient background. When sent from a rectangle, it is the column index. :param RGB: The amount of Red, Green and Blue. Every two items in the list represent the amount of each color in hex - 00 being the minimum and FF being the maximum. :param oval: True if the object being colored is an oval, False if it is a rectangle. :return: A hexadecimal color code. """ color_hex_string = '#' if oval == True: shade = int((index[0] + index[1])) else: shade = int(index) for ind in RGB: color_hex_string += hex(shade + ind)[2:] return color_hex_string def __handle_message(self, text=None): """ Specifies the event handler for the message getting event in the communicator. Upon reception of a message, sends the column number to the event handler. :param text: the number of the column to place a disc in. """ if text: column = int(text) self.event_handler(column, True) if not self.__is_human: self._AI.find_legal_move(self.event_handler) def win(self): """ Sets the winning four animation if there is a win and calls the message box function, otherwise changes the current player. """ game = self._game if game.is_win(): tuple_list, curr_player = game.is_win() if curr_player == game.PLAYER_ONE: outline = '#ff851c' game.set_winner(game.PLAYER_ONE) else: outline = '#00ff00' game.set_winner(game.PLAYER_TWO) for tuple in tuple_list: x, y = tuple item_id = self.__discs[y, x] self._canvas.itemconfig(item_id, outline=outline, dash=5, width=3) self.win_message() if game.get_current_player() == game.PLAYER_ONE: game.set_current_player(game.PLAYER_TWO) else: game.set_current_player(game.PLAYER_ONE) def tie(self): """ The Tie message box. """ if self._game.is_tie(): if messagebox.showinfo('Game Over', 'It\'s a Tie!'): self._master.destroy() def win_message(self): """ The Win Message boxes. """ if self._game.get_current_player() == self._game.PLAYER_ONE: if t.messagebox.showinfo('Game Over!', 'Red Wins'): self._master.destroy() if self._game.get_current_player() == self._game.PLAYER_TWO: if t.messagebox.showinfo('Game Over!', 'Green Wins'): self._master.destroy()
print('Illegal program arguments.') if len(arguments) == 4: server = False return server # False if client. if __name__ == '__main__': # For some reason these imports failed on the computers in the lab when # they were placed up top. from game import Game from ai import AI server = check_arguments() root = t.Tk() root.resizable(False, False) game_object = Game() if server: player = game_object.PLAYER_ONE gui = GUI(game_object, root, int(sys.argv[2]), player, None) if sys.argv[1] == 'ai': gui.set_is_human(False) #if the ai has to make the first move, call the make move function. AI.find_legal_move(gui._AI, gui.event_handler) root.title("Server") else: player = game_object.PLAYER_TWO gui = GUI(game_object, root, int(sys.argv[2]), player, sys.argv[3]) if sys.argv[1] == 'ai': gui.set_is_human(False) root.title("Client") root.mainloop()
class Connect4GUI(t.Frame): """ Designed to handle the GUI aspects (creating a window, buttons and pop-ups. Also initializes the communicator object. """ ############################################################ # Constants ############################################################ #: Defines the game title. GAME_TITLE = "Four in a Row" #: Defines the game title bar text. GAME_TITLEBAR_FORMAT = "Four in a Row (%s player)" #: Defines the invalid play location message. INVALID_LOCATION_MESSAGE = "The selected location is invalid." #: Defines the current player name title. PLAYER_TITLE = "You" #: Defines the opponent player name title. OPPONENT_TITLE = "Opponent" #: Defines the message that will tell the player is playing now. NOW_PLAYING = "Now Playing" #: Defines the moves counter. MOVE_NUMBER_FORMAT = "Move #%d" #: Defines the Win message. CURRENT_PLAYER_WON_MESSAGE = "You've won! You Rocks!" #: Defines the Lose message. CURRENT_PLAYER_LOSE_MESSAGE = "Ugh, You lost..." #: Describe the server connecting message. SERVER_WAITING_MESSAGE = "Waiting for a client (Server IP: %s)..." #: Describe the client connecting message. CLIENT_WAITING_MESSAGE = "Connecting to the server..." #: Describe the opponent thinking message. OPPONENT_THINKING_MESSAGE = "Opponent is thinking, please wait..." #: Describe the AI thinking message. AI_THINKING_MESSAGE = "The Amazing AI is generating move..." #: Defines the game title Y padding. GAME_TITLE_PADDING_Y = 35 #: Defines the screen width. SCREEN_WIDTH = 645 #: Defines the screen height. SCREEN_HEIGHT = 485 #: Defines the width for each board spot (a.k.a disc container). BOARD_LOCATION_SIZE_X = 50 #: Defines the height for each board spot (a.k.a disc container). BOARD_LOCATION_SIZE_Y = 50 #: Defines the board X padding. BOARD_PADDING_X = 20 #: Defines the board Y padding. BOARD_PADDING_Y = 10 #: Defines the status bar Y padding. STATUS_BAR_PADDING_Y = 20 #: Defines the player panels Y padding. PLAYER_PANEL_PADDING_Y = 20 #: Defines the Y padding between the player title and subtitle. PLAYER_PANEL_PADDING_TO_SUBTITLE_Y = 25 #: Defines the Y padding between the player subtitle and image. PLAYER_PANEL_PADDING_TO_IMAGE_Y = 30 #: Defines the images base path. IMAGES_BASE_PATH = 'images' #: Defines the game background image name. IMAGE_BACKGROUND = 'background.jpg' #: Defines the game board image name. IMAGE_BOARD_BACKGROUND = 'board_background.jpg' #: Defines the disc image path (should be formatted). IMAGE_DISC_FORMAT = 'disc_%s.jpg' #: Defines the column highlighter image path.. IMAGE_COLUMN_HIGHLIGHTER = 'highlight_arrow.jpg' #: Defines the default text color. DEFAULT_COLOR = 'white' #: Defines the errors text color. ERROR_TEXT_COLOR = 'red' #: Defines the win message color. CURRENT_PLAYER_WON_COLOR = "green" #: Defines the lose message color. CURRENT_PLAYER_LOSE_COLOR = "red" #: Defines the status bar font. STATUS_BAR_FONT = "Helvetica 22 italic" #: Defines the game title font. TITLE_FONT = "Helvetica 32 bold" #: Defines the player title font. PLAYER_TITLE_FONT = "Helvetica 24" #: Defines the player sub-title font. PLAYER_SUBTITLE_FONT = "Helvetica 16" #: Defines the current turn font. CURRENT_TURN_FONT = "Helvetica 18 bold" #: Defines the moves counter font. MOVES_COUNTER_FONT = "Helvetica 18 italic" #: Defines the board marker fill. BOARD_MARKER_FILL = '' # = transparent #: Defines the board marker weight. BOARD_MARKER_WEIGHT = 7 #: Defines the timeout for a status bar timeout delegate invocation. STATUS_BAR_TIMEOUT_DURATION = 4000 #: Define a time of AI move generation delay. This value must be used # to allow the rendering pipeline to work correctly. AI_RENDER_DELAY = 200 #: The default AI move generation timeout, in seconds. AI_DEFAULT_TIMEOUT = 3 #: Define the angle in which we're gonna move to create a circle # stride. Note that 2 pi radians = 60 degrees POLY_CIRCLE_STRIDE_ANGLE = math.pi * 2 ############################################################ # Ctor ############################################################ def __init__(self, parent, game, player, port, ip=None): """ Initializes the GUI and connects the communicator. :param parent: the tkinter root. :type parent: t.Window :param ip: the ip to connect to. :param port: the port to connect to. """ t.Frame.__init__(self, parent) # Setup the iVars self.__parent = parent self.__game = game self.__ai_engine = AI() self.__current_player = player self.__board_pos_x = 0 self.__board_pos_y = 0 self.__current_turn_pos_y = 0 self.__board_discs = {} self.__current_highlighted_row = None self.__highlighter_res_id = None self.__status_bar_res_id = None self.__status_bar_timeout_res_id = None self.__current_turn_left_res_id = None self.__current_turn_right_res_id = None self.__current_move_res_id = None self.__board_ui_disabled = True self.__is_current_turn = player.get_player_number() == Game.PLAYER_ONE self.__communicator = Connect4Communicator(self.__parent, port, ip) # Initialize the drawing canvas self.__canvas = t.Canvas(self.__parent, width=Connect4GUI.SCREEN_WIDTH, height=Connect4GUI.SCREEN_HEIGHT) self.__canvas.image_resources = {} # Saves us from being GC'ed. # Setup the UI self.__canvas.pack(expand=t.YES, fill=t.BOTH, padx=0, pady=0) self.__init_ui() # Write the title bar text self.__parent.title(Connect4GUI.GAME_TITLEBAR_FORMAT % self.__current_player.get_color()) # Register UI events self.__parent.bind('<Button-1>', self.__handle_mouse_click) self.__parent.bind('<Motion>', self._handle_mouse_move) game.set_on_game_over_callback(self.__handle_game_end) # Start to work on the communication self.__setup_communication_channel() ############################################################ # Private Methods ############################################################ # region Public API def perform_player_move(self, column): """ Perform a move by the currently playing player/ :param column: The played column. :return: True if the move successfully made, false otherwise. """ # Clear the status bar self.truncate_status_bar() # Firstly perform the move locally. if not self.perform_move(self.__current_player, column): return False # Now transfer it to the opponent client. self.__communicator.move_played( self.__current_player.get_player_number(), column) return True def perform_move(self, player, column): """ Performs a single move for the given player. :param player: The playing player instance. :param column: The column to play in. :return: True if the move successfully made, false otherwise. """ # Perform the move in the UI and the game if not self.__perform_move(column): return False # Mark that this is the {player}'s opponent turn self.__is_current_turn = not self.__is_current_turn self.__ui_set_current_turn( self.__game.get_opponent(player.get_player_number())) # Update the moves counter self.__ui_update_moves_counter() return True def add_disc(self, color, row, column): """ Adds a disck to the board. :param color: The disc color. :param row: The row location. :param column: The column location. """ # Get the disc image disc_img = self.__get_image(Connect4GUI.IMAGE_DISC_FORMAT % color) # Compute the right position pos_x = self.__board_pos_x + Connect4GUI.BOARD_PADDING_X + \ Connect4GUI.BOARD_LOCATION_SIZE_X * column pos_y = self.__board_pos_y + Connect4GUI.BOARD_PADDING_Y + \ Connect4GUI.BOARD_LOCATION_SIZE_Y * row # Draw the disc res_id = self.__canvas.create_image(pos_x, pos_y, image=disc_img, anchor=t.NW) self.__board_discs[(row, column)] = res_id def highlight_column(self, column): """ Highlights the given column. :param column: The column number. """ # Do we got a highlighted column? if self.__current_highlighted_row == column: return if self.__highlighter_res_id is not None: self.unhighlight_column() # Get the arrow picture arrow = self.__get_image(Connect4GUI.IMAGE_COLUMN_HIGHLIGHTER) # Calculate the positioning pos_x = self.__board_pos_x + Connect4GUI.BOARD_LOCATION_SIZE_X * \ column + arrow.width() / 2 pos_y = self.__board_pos_y - arrow.height() # Render self.__current_highlighted_row = column self.__highlighter_res_id = self.__canvas.create_image(pos_x, pos_y, image=arrow, anchor=t.NW) def unhighlight_column(self): """ Un-highlight the currently highlighted column. """ if self.__highlighter_res_id is not None: self.__canvas.delete(self.__highlighter_res_id) self.__highlighter_res_id = None self.__current_highlighted_row = None def set_status_bar_text(self, text, color, timeout_delegate=None): """ Sets the status bar text. :param text: The text to put. :param color: The text color. :param timeout_delegate: A delegate to invoke after a given timeout. """ # If we already got content on our status bar, remove it first. if self.__status_bar_res_id is not None: self.truncate_status_bar() # Compute the positioning pos_x = Connect4GUI.SCREEN_WIDTH / 2 pos_y = self.__board_pos_y / 2 + Connect4GUI.STATUS_BAR_PADDING_Y # Render self.__status_bar_res_id = self.__canvas.create_text( pos_x, pos_y, fill=color, font=Connect4GUI.STATUS_BAR_FONT, text=text) # Should we invoke any timeout delegate? if timeout_delegate: # Avoid duplication if self.__status_bar_timeout_res_id is not None: self.after_cancel(self.__status_bar_timeout_res_id) self.__status_bar_timeout_res_id = None # Schedule self.__status_bar_timeout_res_id = self.after( Connect4GUI.STATUS_BAR_TIMEOUT_DURATION, timeout_delegate) def truncate_status_bar(self): """ Truncate the status bar. """ # Did we got a status bar resource? if self.__status_bar_res_id is not None: self.__canvas.delete(self.__status_bar_res_id) self.__status_bar_res_id = None # Clear the status bar timer if self.__status_bar_timeout_res_id is not None: self.after_cancel(self.__status_bar_timeout_res_id) self.__status_bar_timeout_res_id = None # endregion # region UI Events def __handle_game_end(self, game, winner): """ Handle the game over event. :param game: The game instance. :param winner: The winner player number. """ # Update the status bar this_player_won = winner == self.__current_player.get_player_number() if this_player_won: color = Connect4GUI.CURRENT_PLAYER_WON_COLOR self.set_status_bar_text(Connect4GUI.CURRENT_PLAYER_WON_MESSAGE, color) else: color = Connect4GUI.CURRENT_PLAYER_LOSE_COLOR self.set_status_bar_text(Connect4GUI.CURRENT_PLAYER_LOSE_MESSAGE, color) # Mark the winning discs positions initial_coord, end_coord = game.get_board().get_winning_coordinates() self.__ui_mark_board(initial_coord, end_coord, color) # Clearn the current turn indicator self.__ui_clean_current_turn() # Disable the board UI self.__board_ui_disabled = True def __handle_mouse_click(self, event): """ Handle the mouse click event. :param event: The even information. """ if self.__is_board_ui_disabled(): return # Clear the cursor self.unhighlight_column() # Did we selected any column to play in? column = self.__get_mouse_board_column(event.x, event.y) if column is None: return if not self.perform_player_move(column): self.set_status_bar_text(Connect4GUI.INVALID_LOCATION_MESSAGE, Connect4GUI.ERROR_TEXT_COLOR, lambda: self.truncate_status_bar()) def _handle_mouse_move(self, event): """ Handle the mouse click event. :param event: The even information. """ self.__handle_column_highlight(event) # endregion # region Communication def __setup_communication_channel(self): """ Setups the game communication channel. """ # Show the connection message if self.__current_player.get_player_number() == Game.PLAYER_ONE: # This is the server program according to the instructions self.set_status_bar_text( Connect4GUI.SERVER_WAITING_MESSAGE % self.__communicator.get_ip_address(), Connect4GUI.DEFAULT_COLOR) else: # This is the client program self.set_status_bar_text(Connect4GUI.CLIENT_WAITING_MESSAGE, Connect4GUI.DEFAULT_COLOR) # Register the UI events self.__communicator.set_on_connected(self.__on_connected) self.__communicator.set_on_player_selected(self.__on_player_selected) self.__communicator.set_on_move_performed(self.__handle_move_played) # Attempt to connect self.__communicator.connect() def __on_connected(self): """ Handle the "on opponent connected" event. """ # Notify the other player who are we self.__communicator.set_player_type( self.__current_player.get_player_number(), self.__current_player.get_player_type()) def __handle_move_played(self, player, column): """ Handle the "move played" event. :param player: The player who performed the move. :param column: The move column. """ # We shouldn't handle our moves, only our opponent if self.__current_player.get_player_number() == player: return # Perform the move self.perform_move( self.__game.get_opponent( self.__current_player.get_player_number()), column) if self.__game.get_winner() is None: # Do we play as AI? if self.__current_player.get_player_type() == \ Player.PLAYER_TYPE_COMPUTER: self.after(Connect4GUI.AI_RENDER_DELAY, self.__handle_ai_move) def __on_player_selected(self, player_number, player_type): """ Handle the "player selected" (a.k.a. "human"/"ai" chose) event. :param player_number: The player number. :param player_type: The player type. """ # We got a message for our opponent? if self.__current_player.get_player_number() == player_number: return # Update the opponent player information self.__game.get_player(player_number).set_player_type(player_type) # Remove the waiting status bar title. self.truncate_status_bar() # Create the players panels self.__ui_create_player_panel(self.__game.get_player(Game.PLAYER_ONE)) self.__ui_create_player_panel(self.__game.get_player(Game.PLAYER_TWO)) # Enable the UI self.__board_ui_disabled = False # Starts the game self.__start_game() # endregion # region Private Helpers def __get_image(self, image_path): """ Gets an image resource. :param image_path: The image path. :return: The loaded image resource, or the cached one if the image was already loaded. :type ImageTk.PhotoImage: """ image_path = os.path.join(Connect4GUI.IMAGES_BASE_PATH, image_path) if image_path in self.__canvas.image_resources: return self.__canvas.image_resources[image_path] img = Image.open(image_path).convert('RGBA') image = ImageTk.PhotoImage(img) self.__canvas.image_resources[image_path] = image return image def __computes_polygon_oval_coords(self, x0, y0, x1, y1, stride=25, angle=0): """ Compute the coordinates used to draw a polygon based oval with the given axis and rotation. We'll use this function to create rotatable oval. :param x0: The initial x coordinate. :param y0: The initial y coordinate. :param x1: The end x coordinate. :param y1: The end y coordinate. :param stride: The number of steps in the polygon division used to make it looks like a circle. :param angle: The rotation angle in degrees. :return: An array contains the coordinates for the polygon. """ # Converts the angle into radians angle = math.radians(angle) # Gets the major axes major_x = (x1 - x0) / 2.0 major_y = (y1 - y0) / 2.0 # Computes the center location (ex6, lol) center_x = x0 + major_x center_y = y0 + major_y # Computes the oval polygon as a list of coordinates coordinates_list = [] for i in range(stride): # Calculate the angle for this step theta = Connect4GUI.POLY_CIRCLE_STRIDE_ANGLE * (float(i) / stride) x1 = major_x * math.cos(theta) y1 = major_y * math.sin(theta) # Rotate the X and Y coordinates x = (x1 * math.cos(angle)) + (y1 * math.sin(angle)) y = (y1 * math.cos(angle)) - (x1 * math.sin(angle)) # Append them coordinates_list.append(round(x + center_x)) coordinates_list.append(round(y + center_y)) return coordinates_list def __in_board_column(self, column, x, y): """ Determines if the given X and Y coordinates resist in the given X and Y coordinates. :param column: The column index. :param x: The X coordinate. :param y: The Y coordinate. :return: True if the coordinates are in the given column at the board, false otherwise. """ # Compute the positions x0 = self.__board_pos_x + Connect4GUI.BOARD_PADDING_X + \ (column * Connect4GUI.BOARD_LOCATION_SIZE_X) x1 = x0 + Connect4GUI.BOARD_LOCATION_SIZE_X y0 = self.__board_pos_y + Connect4GUI.BOARD_PADDING_Y y1 = y0 + (Board.HEIGHT * Connect4GUI.BOARD_LOCATION_SIZE_Y) # Do we have an intersection? if x0 <= x <= x1 and y0 <= y <= y1: return True return False def __is_board_ui_disabled(self): """ Determine if the board UI is disabled or not. :return: True if the board UI is disabled, false otherwise. """ return self.__board_ui_disabled or not self.__is_current_turn \ or self.__current_player.get_player_type() == \ Player.PLAYER_TYPE_COMPUTER def __get_mouse_board_column(self, x, y): """ Gets the column in which the mouse sits in based on the given coordinates. :param x: The X coordinate. :param y: The Y coordinate. :return: The mouse board column or None if the mouse isn't on the board. """ for c in range(Board.WIDTH): if self.__in_board_column(c, x, y): return c return None def __handle_column_highlight(self, event): """ Handles the row highlight task. :param event: The mouse event container. """ # Is the board disabled? if self.__is_board_ui_disabled(): return # Do we need to highlight any row? highlighted_column = self.__get_mouse_board_column(event.x, event.y) if highlighted_column is None: return # Remove old highlights if self.__current_highlighted_row is not None: self.__current_highlighted_row = None self.unhighlight_column() # Highlight it self.highlight_column(highlighted_column) def __handle_ai_move(self): """ Generates and plays a move by the AI. """ def __do_handle_ai_move(): """ Do the actual AI move handling (after the GUI is updated). """ # Init selected_move = None def move_selected(value): # Notify Python that this is an outer variable. # See: https://stackoverflow.com/a/12182176/1497516 nonlocal selected_move # Save the last value. selected_move = value # Get the move self.__ai_engine.find_legal_move(self.__game, move_selected, Connect4GUI.AI_DEFAULT_TIMEOUT) # Play it self.perform_player_move(selected_move) # Set the AI thinking label for this player so she won't get bored self.set_status_bar_text(Connect4GUI.AI_THINKING_MESSAGE, Connect4GUI.DEFAULT_COLOR) # Do the actual AI handling self.after(Connect4GUI.AI_RENDER_DELAY, __do_handle_ai_move) def __start_game(self): """ Starts the game. """ # Mark the current player self.__ui_set_current_turn(self.__game.get_player(Game.PLAYER_ONE)) # Set the move number self.__ui_update_moves_counter() # We're starting? if self.__is_current_turn: # Is this the AI who playing? if self.__current_player.get_player_type() == \ Player.PLAYER_TYPE_COMPUTER: self.after(Connect4GUI.AI_RENDER_DELAY, self.__handle_ai_move) def __perform_move(self, column): """ Performs a single move. :param column: The column to play in. :return: True if the move have successfully done, false otherwise. """ # Can we perform that move? if not self.__game.is_valid_move(column): return False # Get the disk row row = Board.HEIGHT - 1 - self.__game.get_board() \ .count_column_items(column) player_color = self.__game.get_player( self.__game.get_current_player()).get_color() # Put the disc in our lovely gui self.add_disc(player_color, row, column) # Perform the move self.__game.make_move(column) # Re-render the current moves counter self.__ui_update_moves_counter() return True def __init_ui(self): """ Initialize and render the game UI. """ # Create the background self.__ui_create_background() # Create the background self.__ui_create_board() # Disable the option to resize the window self.__parent.resizable(False, False) def __ui_create_background(self): """ Renders the game background (and the title... haha). """ # Background Image bg_image = self.__get_image(Connect4GUI.IMAGE_BACKGROUND) self.__canvas.create_image(0, 0, image=bg_image, anchor=t.NW) # The game title self.__canvas.create_text(Connect4GUI.SCREEN_WIDTH / 2, Connect4GUI.GAME_TITLE_PADDING_Y, fill=Connect4GUI.DEFAULT_COLOR, font=Connect4GUI.TITLE_FONT, text=Connect4GUI.GAME_TITLE) def __ui_create_board(self): """ Renders the board. """ # Load the background board_bg = self.__get_image(Connect4GUI.IMAGE_BOARD_BACKGROUND) # Position in the center of the screen self.__board_pos_x = math.floor( (Connect4GUI.SCREEN_WIDTH - board_bg.width()) / 2) self.__board_pos_y = math.floor( (Connect4GUI.SCREEN_HEIGHT - board_bg.height()) / 1.4) self.__canvas.create_image(self.__board_pos_x, self.__board_pos_y, image=board_bg, anchor=t.NW) def __ui_create_player_panel(self, player): """ Renders the player panel. :param player: The player instance we're rendering the panel for. :param is_left_panel: True if this is the left panel, false otherwise. """ # Setup is_left_panel = self.__ui_get_left_player(player) pos_x = 0 pos_y = self.__board_pos_y + Connect4GUI.PLAYER_PANEL_PADDING_Y # Resolve data based on the panel location if is_left_panel: pos_x = self.__board_pos_x / 2 else: pos_x = self.__board_pos_x * 1.5 + self.__get_image( Connect4GUI.IMAGE_BOARD_BACKGROUND).width() # Render the player title if self.__current_player.get_player_number() == \ player.get_player_number(): player_title = Connect4GUI.PLAYER_TITLE else: player_title = Connect4GUI.OPPONENT_TITLE self.__canvas.create_text( pos_x, pos_y, # fill=player.get_color(), font=Connect4GUI.PLAYER_TITLE_FONT, text=player_title) # Create the subtitle pos_y += Connect4GUI.PLAYER_PANEL_PADDING_TO_SUBTITLE_Y self.__canvas.create_text(pos_x, pos_y, fill=player.get_color(), font=Connect4GUI.PLAYER_SUBTITLE_FONT, text=player.get_name()) # Render the disc disc_img = self.__get_image(Connect4GUI.IMAGE_DISC_FORMAT % player.get_color()) pos_y += Connect4GUI.PLAYER_PANEL_PADDING_TO_IMAGE_Y + \ disc_img.height() / 2 # Compute the right position self.__canvas.create_image(pos_x, pos_y, image=disc_img) # Save this position so we can use it later. self.__current_turn_pos_y = pos_y + disc_img.height() def __ui_get_left_player(self, player): """ Determine if this is the left panel player. :param player: The player instance. :return: True if this is the left panel player, false otherwise. """ if player.get_player_number() == Game.PLAYER_ONE: return True return False def __ui_set_current_turn(self, player): """ Update the UI to show the current player. :param player: The player. """ # Resolve the panel is_left_panel = self.__ui_get_left_player(player) if is_left_panel: # We already have that panel? if self.__current_turn_left_res_id is not None: return # Get the x position pos_x = self.__board_pos_x / 2 # Remove the other canvas if self.__current_turn_right_res_id is not None: self.__canvas.delete(self.__current_turn_right_res_id) self.__current_turn_right_res_id = None else: # We already have that panel? if self.__current_turn_right_res_id is not None: return # Resolve the position pos_x = self.__board_pos_x * 1.5 + self.__get_image( Connect4GUI.IMAGE_BOARD_BACKGROUND).width() # Remove the other canvas if self.__current_turn_left_res_id is not None: self.__canvas.delete(self.__current_turn_left_res_id) self.__current_turn_left_res_id = None pos_y = self.__current_turn_pos_y res_id = self.__canvas.create_text(pos_x, pos_y, fill=Connect4GUI.DEFAULT_COLOR, font=Connect4GUI.CURRENT_TURN_FONT, text=Connect4GUI.NOW_PLAYING) if is_left_panel: self.__current_turn_left_res_id = res_id else: self.__current_turn_right_res_id = res_id # If this is the opponent player, notify the user via the status bar if self.__current_player != player: self.set_status_bar_text(Connect4GUI.OPPONENT_THINKING_MESSAGE, Connect4GUI.DEFAULT_COLOR) else: self.truncate_status_bar() def __ui_clean_current_turn(self): """ Cleans the current turn indicator. """ # Left panel if self.__current_turn_left_res_id is not None: self.__canvas.delete(self.__current_turn_left_res_id) self.__current_turn_left_res_id = None # Right panel if self.__current_turn_right_res_id is not None: self.__canvas.delete(self.__current_turn_right_res_id) self.__current_turn_left_res_id = None def __ui_update_moves_counter(self): """ Sets the move number in the UI. :param number: The move number. """ # Remove the old label if self.__current_move_res_id is not None: self.__canvas.delete(self.__current_move_res_id) self.__current_move_res_id = None # Compute the next move move_number = self.__game.get_moves_count() + 1 # Compute the positions board_bg = self.__get_image(Connect4GUI.IMAGE_BOARD_BACKGROUND) pos_x = Connect4GUI.SCREEN_WIDTH / 2 pos_y = self.__board_pos_y + board_bg.height() + 20 self.__current_move_res_id = self.__canvas.create_text( pos_x, pos_y, fill=Connect4GUI.DEFAULT_COLOR, font=Connect4GUI.MOVES_COUNTER_FONT, text=(Connect4GUI.MOVE_NUMBER_FORMAT % move_number)) def __ui_mark_board(self, initial_coord, end_coord, color): """ Marks the given coordinates on the board. :param initial_coord: The initial coordinate. :param end_coord: The end coordinate. :param color: The color to use to mark the coordinates with. """ # Calculate the marking oval coordinates based on the win sequence. angle = 0 is_left_diagonal = False is_right_diagonal = False if initial_coord[1] == end_coord[1]: # Vertical win, so we'll mark one column on the x and the # difference between the columns in the y. diff_x = 1 diff_y = (end_coord[0] - initial_coord[0]) + 1 elif initial_coord[0] == end_coord[0]: # This is an horizontal win, so we'll mark one column on the y # and the difference between the columns in the x. diff_x = (end_coord[1] - initial_coord[1]) + 1 diff_y = 1 else: # This is a digaonal win. Now we need to determine where it # points to? It goes from top left to bottom right ("\") or from # bottom left to top right ("/"). if initial_coord[1] < end_coord[1]: # This is a top left to bottom right diagonal. diff_x = (end_coord[1] - initial_coord[1]) + 1 diff_y = 1 angle = 45 # 45" clockwise is_left_diagonal = True else: # This is a top right to bottom left diagonal. Thus we need # to firstly swap between the coordinates so we make sure # the min coordinate is less then the big one. tmp = initial_coord initial_coord = end_coord end_coord = tmp # And now we're save to calculate as we normally do diff_x = (end_coord[1] - initial_coord[1]) + 1 diff_y = 1 angle = -45 # 45" anti-clockwise is_right_diagonal = True # Computes the (x0, y0) and (x1, y1) coordinates x0 = self.__board_pos_x + Connect4GUI.BOARD_PADDING_X x0 += (initial_coord[1] * Connect4GUI.BOARD_LOCATION_SIZE_X) x1 = x0 + (diff_x * Connect4GUI.BOARD_LOCATION_SIZE_X) y0 = self.__board_pos_y + Connect4GUI.BOARD_PADDING_Y y0 += (initial_coord[0] * Connect4GUI.BOARD_LOCATION_SIZE_Y) y1 = y0 + (diff_y * Connect4GUI.BOARD_LOCATION_SIZE_Y) # If we had a diagonal, we need to to re-adjust the x and y # coordinates based on the the rotation changes. Thus, we need to # push the x and y coordinates accordingly to the X difference. if is_left_diagonal or is_right_diagonal: diagonal_diff = max(diff_x - 1, 1) / 2 if is_left_diagonal: x0 -= Connect4GUI.BOARD_LOCATION_SIZE_X * diagonal_diff x1 += Connect4GUI.BOARD_LOCATION_SIZE_X * diagonal_diff y0 -= Connect4GUI.BOARD_LOCATION_SIZE_Y * diagonal_diff y1 -= Connect4GUI.BOARD_LOCATION_SIZE_Y * diagonal_diff else: x0 -= Connect4GUI.BOARD_LOCATION_SIZE_X * diagonal_diff x1 += Connect4GUI.BOARD_LOCATION_SIZE_X * diagonal_diff y0 += Connect4GUI.BOARD_LOCATION_SIZE_Y * diagonal_diff y1 += Connect4GUI.BOARD_LOCATION_SIZE_Y * diagonal_diff # Render self.__canvas.create_polygon(self.__computes_polygon_oval_coords( x0, y0, x1, y1, angle=angle), outline=color, width=Connect4GUI.BOARD_MARKER_WEIGHT, fill=Connect4GUI.BOARD_MARKER_FILL)