diff --git a/bot.py b/bot.py index 588f967..d99a136 100644 --- a/bot.py +++ b/bot.py @@ -18,6 +18,8 @@ def __choose_name(self) -> str: return choice(self.NAMES) def __negamax(self, board, depth, alhpa, beta, multiplier) -> tuple: + pass + """ if depth == 0 or self.__rules.check_game_over(board): return (multiplier * score) @@ -31,10 +33,14 @@ def __negamax(self, board, depth, alhpa, beta, multiplier) -> tuple: break return (best_value, -1) + """ def get_move(self, board) -> int: + pass + """ # "negamax" algorithm determines best move return self.__negamax(board, self.__depth, -999, 999, 1)[1] + """ @property def name(self): diff --git a/db.sqlite b/db.sqlite index 8734f31..440720d 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/fancy_print.py b/fancy_print.py index 13990aa..48408ed 100644 --- a/fancy_print.py +++ b/fancy_print.py @@ -1,21 +1,17 @@ class FancyPrint: - #HEADER = '\033[95m' - #OKBLUE = '\033[94m' - #UNDERLINE = '\033[4m' + # declare needed ANSI escape sequences for colorful output to console + __BLUE = '\033[94m' + __RED = '\033[91m' + __YELLOW = '\033[93m' + __END = '\033[0m' - def __init__(self) -> None: - # declare needed ANSI escape sequences for colorful output to console - self.__RED = '\033[91m' - self.__YELLOW = '\033[93m' - self.__BLUE = '\033[94m' - self.__BOLD = '\033[1m' - self.__END = '\033[0m' + # string "message" will be displayed in the corresponding color + # when highlighting a single character, "line_end" can be modified so that no line break is added after the message + def blue(self, message: str, line_end: str) -> None: + print(type(self).__BLUE + message + type(self).__END, end=line_end) - def red(self, message, line_end='\n') -> None: - print(self.__RED + message + self.__END, end=line_end) + def red(self, message: str, line_end: str) -> None: + print(type(self).__RED + message + type(self).__END, end=line_end) - def blue(self, message, line_end='\n') -> None: - print(self.__BLUE + message + self.__END, end=line_end) - - def yellow(self, message, line_end='\n') -> None: - print(self.__YELLOW + message + self.__END, end=line_end) # TODO: bold? + def yellow(self, message: str, line_end: str) -> None: + print(type(self).__YELLOW + message + type(self).__END, end=line_end) diff --git a/game.py b/game.py index 04a18a2..2b4e5b0 100644 --- a/game.py +++ b/game.py @@ -1,6 +1,5 @@ from board import Board from bot import Bot -from fancy_print import FancyPrint from gui import GUI from player import Player from rules import Rules @@ -12,7 +11,6 @@ def __init__(self) -> None: self.__board = Board() self.__gui = GUI() self.__rules = Rules() - self.__fancy_print = FancyPrint() # player objects will be set in "initialize_match" self.__player_1 = None self.__player_2 = None @@ -25,14 +23,14 @@ def __init__(self) -> None: self.__play() def __welcome(self) -> None: - self.__fancy_print.blue("Ready to play CONNECT FOUR?") - self.__fancy_print.blue("Here we go!") + self.__gui.text_blue("Ready to play CONNECT FOUR?") + self.__gui.text_blue("Here we go!") def __human_player_wanted(self, player_number: int) -> bool: # inform user about options - print("Player {} shall be:".format(player_number)) - print("-> human (press 'H')") - print("-> a bot (press 'B')") + self.__gui.text("Player {} shall be:".format(player_number)) + self.__gui.text("-> human (press 'H')") + self.__gui.text("-> a bot (press 'B')") # ask for input until valid decision has been made while True: # let user choose type of player (human or bot) by pressing the corresponding key @@ -41,26 +39,26 @@ def __human_player_wanted(self, player_number: int) -> bool: return True elif player_type == 'b': return False - else: - self.__fancy_print.red("ERROR: Press 'H' or 'B'.") + self.__gui.text_red("ERROR: Press 'H' or 'B'.") def __initialize_match(self) -> None: # create either objects of "Player" or "Bot" for both players - this is decided in "human_player_wanted" - # initialization of player names and symbols for representation on board is done in "Player" and "Bot" + # initialization of player names and symbols for representation on board is done in "Player" or "Bot" if self.__human_player_wanted(1): - self.__player_1 = Player(1, self.__player_1_default_symbol) + self.__player_1 = Player(1, self.__player_1_default_symbol, self.__gui) else: - Bot(1, self.__player_1_default_symbol) + self.__player_1 = Bot(1, self.__player_1_default_symbol, self.__gui) + if self.__human_player_wanted(2): - self.__player_2 = Player(2, self.__player_2_default_symbol) + self.__player_2 = Player(2, self.__player_2_default_symbol, self.__gui) else: - Bot(2, self.__player_2_default_symbol) + self.__player_2 = Bot(2, self.__player_2_default_symbol, self.__gui) # ensure that players have unique names and symbols if self.__player_1.name == self.__player_2.name or self.__player_1.symbol == self.__player_2.symbol: if type(self.__player_1) == Player and type(self.__player_2) == Player: # if both players are human, restart initialization - self.__fancy_print.red("ERROR: Both players have the same name or symbol! Try again.") + self.__gui.text_red("ERROR: Both players have the same name or symbol! Try again.") self.__initialize_match() return # abort current call of "initialize_match" since new one has been created else: @@ -83,23 +81,27 @@ def __initialize_match(self) -> None: self.__id_to_symbol[self.__player_2.id] = self.__player_2.symbol # summarize player information before exiting initialization - self.__fancy_print.blue("\nGreat. Let's start the game!") - print("Player 1 ({}): {} is '{}'.".format("HUMAN" if type(self.__player_1) == Player else "BOT", - self.__player_1.name, self.__player_1.symbol)) - print("Player 2 ({}): {} is '{}'.\n".format("HUMAN" if type(self.__player_2) == Player else "BOT", - self.__player_2.name, self.__player_2.symbol)) + self.__gui.text_blue("\nGreat. Let's start the game!") + self.__gui.text("Player 1 ({}): {} is '{}'.".format("HUMAN" if type(self.__player_1) == Player else "BOT", + self.__player_1.name, self.__player_1.symbol)) + self.__gui.text("Player 2 ({}): {} is '{}'.\n".format("HUMAN" if type(self.__player_2) == Player else "BOT", + self.__player_2.name, self.__player_2.symbol)) def __move(self, player: Player) -> None: # a move of a bot will always be valid - thus, only for a human player further checking is needed if type(player) == Bot: - self.__board.do_move(player.get_move(), player.id) + self.__board.do_move(player.get_move(self.__gui), player.id) else: while True: - # "player.get_move" asks user for valid input (integer between 1 and 7) - desired_move = player.get_move() + # "player.get_move" asks user for valid input (when width is 7: integer between 0 and 6) + desired_move = player.get_move(self.__gui) # "rules.is_move_possible" checks if desired move is not against the rules if self.__rules.check_move(self.__board.get_board(), self.__board.height, desired_move): break + else: + # let user know the move is not possible + # "desired_move" is zero-based and needs to be increased by one for display + self.__gui.text_red("Column {} is already full! Try again.".format(desired_move + 1)) # when a legal move is given, "board.do_move" organizes actually playing it self.__board.do_move(desired_move, player.id) @@ -109,45 +111,48 @@ def __play_match(self) -> None: while True: match_round += 1 # before asking a player what to do, show the board - self.__gui.show(self.__board.get_board(), self.__board.width, self.__board.height, self.__id_to_symbol) + self.__gui.show_board(self.__board.get_board(), self.__board.width, self.__board.height, + self.__id_to_symbol) # depending on who's turn it is, let player do a move if match_round % 2 == 1: # prompt player to do a move with method "move" self.__move(self.__player_1) - # check if player 1 has won by playing the latest move + # check if player 1 has won with his latest move winning_line = self.__rules.check_win(self.__board.get_board(), self.__board.width, self.__board.height, self.__board.get_last_move(), self.__player_1.id) # "winning_line" is either None or a list of tuples that store positions of tokens which led to win - if winning_line: - # player 1 has won; show board (emphasizing winning line) one last time - self.__gui.show(self.__board.get_board(), self.__board.width, self.__board.height, - self.__id_to_symbol, winning_line) - self.__fancy_print.blue("{} has won. Good game!".format(self.__player_1.name)) + if winning_line is not None: + # player 1 has won; show board - emphasizing the winning line - one last time before exiting method + self.__gui.show_board(self.__board.get_board(), self.__board.width, self.__board.height, + self.__id_to_symbol, winning_line) + self.__gui.text_blue("{} has won. Good game!".format(self.__player_1.name)) # increase the score of player 1 by one before exiting method self.__player_1.score += 1 return else: # prompt player to do a move with method "move" self.__move(self.__player_2) - # check if player 2 has won by playing the latest move + # check if player 2 has won with his latest move winning_line = self.__rules.check_win(self.__board.get_board(), self.__board.width, self.__board.height, self.__board.get_last_move(), self.__player_2.id) # "winning_line" is either None or a list of tuples that store positions of tokens which led to win - if winning_line: - # player 2 has won; show board (emphasizing winning line) one last time - self.__gui.show(self.__board.get_board(), self.__board.width, self.__board.height, - self.__id_to_symbol, winning_line) - self.__fancy_print.blue("{} has won. Good game!".format(self.__player_2.name)) + if winning_line is not None: + # player 2 has won; show board - emphasizing the winning line - one last time before exiting method + self.__gui.show_board(self.__board.get_board(), self.__board.width, self.__board.height, + self.__id_to_symbol, winning_line) + self.__gui.text_blue("{} has won. Good game!".format(self.__player_2.name)) # increase the score of player 2 by one before exiting method self.__player_2.score += 1 return # if board is full, show it one last time and let players know that match is a draw before exiting method if self.__rules.check_game_over(self.__board.get_board(), self.__board.width, self.__board.height): - self.__gui.show(self.__board.get_board(), self.__board.width, self.__board.height, self.__id_to_symbol) - self.__fancy_print.blue("It's a draw!") + self.__gui.show_board(self.__board.get_board(), self.__board.width, self.__board.height, + self.__id_to_symbol) + self.__gui.text_blue("It's a draw!") return + """ def __settings(self) -> None: while True: input_change_player_settings = input("Do you want to change any player settings? (Y/N): ").lower() @@ -157,21 +162,23 @@ def __settings(self) -> None: self.__fancy_print.blue("Alright. Next match starts now!") break self.__fancy_print.red("ERROR: Press 'Y' or 'N'.") + """ def __goodbye(self) -> None: - # self.__show_stats - self.__fancy_print.blue("Thank you for playing. Bye for now!") + # TODO: self.__show_stats + self.__gui.text_blue("Thank you for playing. Bye for now!") def __keep_playing(self) -> bool: + # ask user for input until valid decision has been made while True: input_keep_playing = input("What a match! Would you like to play another one? (Y/N) ").lower() if input_keep_playing == 'y': - self.__fancy_print.blue("Cool, next match starts now!") + self.__gui.text_blue("Cool, next match starts now!") return True if input_keep_playing == 'n': self.__goodbye() return False - self.__fancy_print.red("ERROR: Press 'Y' or 'N'.") + self.__gui.text_red("ERROR: Press 'Y' or 'N'.") def __play(self) -> None: # welcome players once in the beginning diff --git a/gui.py b/gui.py index 8aa3a37..0655a78 100644 --- a/gui.py +++ b/gui.py @@ -6,34 +6,48 @@ def __init__(self) -> None: # for better visualization, an instance of "FancyPrint" is needed self.__fancy_print = FancyPrint() - def show(self, board: dict, width: int, height: int, id_to_symbol: dict, winning_line: bool = False) -> None: + # this method simply prints the text; this makes the GUI more modular and exchangeable + def text(self, message, line_end: str = '\n'): + print(message, end=line_end) + + # use "fancy_print" for displaying colorful text + def text_blue(self, message: str, line_end: str = '\n') -> None: + self.__fancy_print.blue(message, line_end) + + def text_red(self, message: str, line_end: str = '\n') -> None: + self.__fancy_print.red(message, line_end) + + def text_yellow(self, message: str, line_end: str = '\n') -> None: + self.__fancy_print.yellow(message, line_end) + + def show_board(self, board: dict, width: int, height: int, id_to_symbol: dict, winning_line: list = None) -> None: # firstly, print column names (usually from 1 to 7) - print(' ' + ' '.join(map(str, range(1, width + 1)))) + self.text(' ' + ' '.join(map(str, range(1, width + 1)))) # if a player has won, highlight the winning line - if winning_line: + if winning_line is None: # because (0, 0) is the bottom-left corner of board, y has to iterate backwards through rows for y in range(height - 1, -1, -1): - # output left barrier - print(end="|") + # display left barrier + self.text("", '|') # print player symbols instead of player id (unoccupied fields are represented as spaces) for each field # columns are separated using the pipe symbol for x in range(width): - if (x, y) in winning_line: - # highlight the symbol on this field to show the winning line - self.__fancy_print.yellow(id_to_symbol[board[(x, y)]], '|') - else: - # a standard field is printed with default font style - print(id_to_symbol[board[(x, y)]], end="|") - # print a line break so that next row starts in the next line - print() + self.text(id_to_symbol[board[(x, y)]], '|') + # add a line break so that next row starts in next line + self.text("") else: # because (0, 0) is the bottom-left corner of board, y has to iterate backwards through rows for y in range(height - 1, -1, -1): - # output left barrier - print(end="|") + # display left barrier + self.text("", '|') # print player symbols instead of player id (unoccupied fields are represented as spaces) for each field # columns are separated using the pipe symbol for x in range(width): - print(id_to_symbol[board[(x, y)]], end="|") - # print a line break so that next row starts in the next line - print() + if (x, y) in winning_line: + # highlight the symbol on this field to show part of the winning line + self.text_yellow(id_to_symbol[board[(x, y)]], '|') + else: + # a standard field is printed with default font style + self.text(id_to_symbol[board[(x, y)]], '|') + # add a line break so that next row starts in next line + self.text("") diff --git a/main.py b/main.py index 1293a42..44d9793 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ from game import Game if __name__ == "__main__": + # main.py simply instantiates an Game object which will take care of the further game process Game() diff --git a/player.py b/player.py index 9326d5a..8427ecb 100644 --- a/player.py +++ b/player.py @@ -1,5 +1,5 @@ from random import choice -from fancy_print import FancyPrint +from gui import GUI class Player: @@ -483,15 +483,14 @@ class Player: "Zoe", "Zofia", "Zoila", "Zola", "Zona", "Zonia", "Zora", "Zoraida", "Zula", "Zulema", "Zulma" ) - def __init__(self, id: int, default_symbol: str) -> None: - self.__fancy_print = FancyPrint() + def __init__(self, id: int, default_symbol: str, gui: GUI) -> None: # set variables (some with help of user) self.__id = id self.__score = 0 - self.__name = self.__choose_name() - self.__symbol = self.__choose_symbol(default_symbol) + self.__name = self.__choose_name(gui) + self.__symbol = self.__choose_symbol(default_symbol, gui) - def __choose_name(self) -> str: + def __choose_name(self, gui: GUI) -> str: while True: input_name = input("Player {}: Enter your name or hit ENTER to get a random one: ".format(self.__id)) if len(input_name) == 0: @@ -501,9 +500,9 @@ def __choose_name(self) -> str: # this program is tolerant with user input - but a too long name is prevented return input_name # invalid input leads to a warning before program asks for input again - self.__fancy_print.red("ERROR: Your name is too long. Try again.") + gui.text_red("ERROR: Your name is too long. Try again.") - def __choose_symbol(self, default_symbol: str) -> str: + def __choose_symbol(self, default_symbol: str, gui: GUI) -> str: while True: input_symbol = input( "{}: Enter your character or hit ENTER to use default symbol '{}':".format(self.__name, default_symbol)) @@ -515,12 +514,12 @@ def __choose_symbol(self, default_symbol: str) -> str: # correct input is returned return input_symbol else: - self.__fancy_print.red("ERROR: This symbol is not allowed! Try again.") + gui.text_red("ERROR: This symbol is not allowed! Try again.") continue # only one character is allowed since otherwise the gui couldn't display the board well - self.__fancy_print.red("ERROR: That's more than one character. Try again.") + gui.text_red("ERROR: That's more than one character. Try again.") - def get_move(self) -> None: + def get_move(self, gui: GUI) -> int: while True: input_move = input("{}: Enter column number to insert token: ".format(self.__name)) try: @@ -529,10 +528,10 @@ def get_move(self) -> None: # return zero-based number for further handling within program return input_move_integer - 1 # if input number is not between 1 and 7, user needs to give input again - self.__fancy_print.red("ERROR: There are only columns 1 - 7! Try again.") + gui.text_red("ERROR: There are only columns 1 - 7! Try again.") except ValueError: # only integers are accepted; a value error leads to a warning and user has to try once more - self.__fancy_print.red("ERROR: Input is not a number! Try again.") + gui.text_red("ERROR: Input is not a number! Try again.") # "@property" decorator makes variables accessible from outside this class even though they're private @property diff --git a/rules.py b/rules.py index ffba70d..ccb7a12 100644 --- a/rules.py +++ b/rules.py @@ -1,19 +1,15 @@ -from fancy_print import FancyPrint - - class Rules: - def __init__(self) -> None: - self.__fancy_print = FancyPrint() - # these tuples denote changes in x and y for the 8 adjacent neighbors of any field (N, NW, W, SW, S, SE, E, NE) - self.__x_directions = (0, 1, 1, 1, 0, -1, -1, -1) - self.__y_directions = (1, 1, 0, -1, -1, -1, 0, 1) + # these tuples denote changes in x and y for 4 of 8 adjacent neighbors of any field (N, NW, W, SW) + # to get the 4 neighbors on the other side, simply multiply the x and y values by -1 + __X_DIRECTIONS = (0, 1, 1, 1) + __Y_DIRECTIONS = (1, 1, 0, -1) def __on_board(self, x: int, y: int, width: int, height: int) -> bool: # check if (x, y) is a valid position on the board return -1 < x < width and -1 < y < height - def check_win(self, board: dict, width: int, height: int, last_move: tuple, player_number: int) -> bool: - # check for at least four consecutive tokens from one player; search order is: + def check_win(self, board: dict, width: int, height: int, last_move: tuple, player_number: int) -> list: + # check for at least four consecutive tokens from player that did last move; search order is: # 1. vertical # 2. upper left - lower right # 3. horizontal @@ -22,14 +18,14 @@ def check_win(self, board: dict, width: int, height: int, last_move: tuple, play # "line" stores all connected tokens that may be needed for display later # position of latest move will always be connected to this line, thus it's included from the beginning line = [last_move] - # starting from last move's position, search for adjacent neighbors in both directions (e.g. up and down) - for direction in range(2): + # orientation denotes the search direction of the line (e.g. up or down) + for orientation in [1, -1]: x, y = last_move while True: - # alter "x" and "y" to get to next field; "direction" * 4 is added to original alignment in order to - # explore both directions of each alignment (e.g. up and down) - x += self.__x_directions[alignment + direction * 4] - y += self.__y_directions[alignment + direction * 4] + # alter "x" and "y" to get to next field; "direction" is multiplied to original alignment in order + # to explore both directions of each alignment (e.g. up and down) + x += self.__X_DIRECTIONS[alignment] * orientation + y += self.__Y_DIRECTIONS[alignment] * orientation # check if field exists before accessing it if self.__on_board(x, y, width, height): if board[(x, y)] == player_number: @@ -44,17 +40,12 @@ def check_win(self, board: dict, width: int, height: int, last_move: tuple, play # if a player has won, return the winning line if len(line) > 3: return line - # if no win has been detected until now, return False instead of a list of tuples - return False + # if no win has been detected until now, return None (automatically done by Python) instead of a list of tuples def check_game_over(self, board: dict, width: int, height: int) -> bool: # if all fields in the top row are occupied, the game is over - return not all(board[(x, height - 1)] == 0 for x in range(width)) + return all(board[(x, height - 1)] != 0 for x in range(width)) def check_move(self, board: dict, height: int, column: int) -> bool: # check if there is at least one free field in desired column by looking at value of top field - if board[(column, height - 1)] == 0: - return True - # let user know the move is not possible - "column" is zero-based and needs to be increased by one for display - self.__fancy_print.fail("Column {} is already full! Try again.".format(column + 1)) - return False + return board[(column, height - 1)] == 0