From 259d05804c04f5c8e76b9531a16996a4e0f6e7f2 Mon Sep 17 00:00:00 2001 From: Pablo <72697714+Pabloo22@users.noreply.github.com> Date: Mon, 8 Mar 2021 16:34:26 +0100 Subject: [PATCH 1/4] Add files via upload --- agents.py | 24 +++++ connect_game.py | 97 ++++++++++++++++--- game.py | 24 ++--- game_AI_vs_AI.py | 105 +++++++++++++++++++++ game_board.py | 221 ++++++++++++++++++++++++++++++++++---------- game_data.py | 1 + game_human_vs_AI.py | 110 ++++++++++++++++++++++ 7 files changed, 507 insertions(+), 75 deletions(-) create mode 100644 agents.py create mode 100644 game_AI_vs_AI.py create mode 100644 game_human_vs_AI.py diff --git a/agents.py b/agents.py new file mode 100644 index 0000000..86b5abe --- /dev/null +++ b/agents.py @@ -0,0 +1,24 @@ +from random import choice +from game_data import GameData +import abc + + +class Agent(abc.ABC): + """ + It is an abstract class. All the agents should inheritance from this class. + """ + + @staticmethod + def get_move(game_data: GameData) -> int: + pass + + +class RandomAgent(Agent): + """ + An agent which makes random moves + """ + + @staticmethod + def get_move(data) -> int: + """ returns a random valid col""" + return choice([c for c in range(7) if data.game_board.is_valid_location(c)]) diff --git a/connect_game.py b/connect_game.py index c5fe4a9..b9aacf8 100644 --- a/connect_game.py +++ b/connect_game.py @@ -9,6 +9,9 @@ from game_data import GameData from game_renderer import GameRenderer +from typing import List +from agents import Agent + class ConnectGame: """ @@ -47,6 +50,29 @@ def mouse_click(self, event: MouseClickEvent): col: int = int(math.floor(event.posx / self.game_data.sq_size)) + self.do_movement(col) + + @bus.on("game:undo") + def undo(self): + """ + Handles the Ctrl+Z keyboard sequence, which + is used to roll back the last move. + :return: + """ + if self.game_data.last_move_row: + + self.game_data.last_move_row.pop() + self.game_data.last_move_col.pop() + + self.game_data.game_board.slots_filled -= 1 + self.game_data.turn += 1 + self.game_data.turn = self.game_data.turn % 2 + + def do_movement(self, col: int): + """ + Allows to make a movement without a mouse click. + Inserts a new piece in the specified column and prints the new board + """ if self.game_data.game_board.is_valid_location(col): row: int = self.game_data.game_board.get_next_open_row(col) @@ -62,7 +88,7 @@ def mouse_click(self, event: MouseClickEvent): self.print_board() - if self.game_data.game_board.winning_move(self.game_data.turn + 1): + if self.game_data.game_board.winning_move(self.game_data.turn + 1, row, col): bus.emit( "game:over", self.renderer, GameOver(False, self.game_data.turn + 1) ) @@ -73,22 +99,65 @@ def mouse_click(self, event: MouseClickEvent): self.game_data.turn += 1 self.game_data.turn = self.game_data.turn % 2 - @bus.on("game:undo") - def undo(self): + @staticmethod + def compare_agents(agent1: Agent, agent2: Agent, n=100, alternate=True) -> List[int]: """ - Handles the Ctrl+Z keyboard sequence, which - is used to roll back the last move. - :return: + The 2 given agents will play between them n times. The games are not showed. + :param agent1: an AI agent + :param agent2: an AI agent + :param n: number of matches + :param alternate: if True agent1 and agent2 will play first the same number of times + :returns: number of [ties, agent1 wins, agent2 wins] """ - if self.game_data.last_move_row: - self.game_data.game_board.drop_piece( - self.game_data.last_move_row.pop(), - self.game_data.last_move_col.pop(), - 0, - ) - self.game_data.turn += 1 - self.game_data.turn = self.game_data.turn % 2 + def play_game(agent1: Agent, agent2: Agent) -> int: + """ + Agent1 plays first, agent2 plays second + :param agent1: an AI agent + :param agent2: an AI agent + :returns: the winner; 1 = agent1, 2 = agent2, 0 = tie + """ + data = GameData() + board = data.game_board + while True: + col = agent1.get_move(data) + row = board.get_next_open_row(col) + board.drop_piece(row, col, 1) + if board.winning_move(1, row, col): + return 1 + + data.turn += 1 + data.turn = data.turn % 2 + + col = agent2.get_move(data) + row = board.get_next_open_row(col) + board.drop_piece(row, col, 2) + + if board.winning_move(2, row, col): + return 2 + + if board.tie_move(): + return 0 + + stats = [0, 0, 0] + if alternate: + for _ in range(n // 2): + winner = play_game(agent1, agent2) + stats[winner] += 1 + + winner = play_game(agent2, agent1) + if winner == 1: + stats[2] += 1 + elif winner == 2: + stats[1] += 1 + else: + stats[0] += 1 + else: + for _ in range(n): + winner = play_game(agent1, agent2) + stats[winner] += 1 + + return stats def update(self): """ diff --git a/game.py b/game.py index 5f16d96..acdfb2c 100644 --- a/game.py +++ b/game.py @@ -3,7 +3,7 @@ import pygame from pygame.locals import KEYDOWN -from config import black, blue, white +from config import black, white from connect_game import ConnectGame from events import MouseClickEvent, MouseHoverEvent, bus from game_data import GameData @@ -51,15 +51,15 @@ def start(): def text_objects(text, font, color): - textSurface = font.render(text, True, color) - return textSurface, textSurface.get_rect() + text_surface = font.render(text, True, color) + return text_surface, text_surface.get_rect() def message_display(text, color, p, q, v): - largeText = pygame.font.SysFont("monospace", v) - TextSurf, TextRect = text_objects(text, largeText, color) - TextRect.center = (p, q) - screen.blit(TextSurf, TextRect) + large_text = pygame.font.SysFont("monospace", v) + text_surf, text_rect = text_objects(text, large_text, color) + text_rect.center = (p, q) + screen.blit(text_surf, text_rect) pygame.init() @@ -82,15 +82,15 @@ def button(msg, x, y, w, h, ic, ac, action=None): if x + w > mouse[0] > x and y + h > mouse[1] > y: pygame.draw.rect(screen, ac, (x, y, w, h)) - if click[0] == 1 and action != None: + if click[0] == 1 and action is not None: action() else: pygame.draw.rect(screen, ic, (x, y, w, h)) - smallText = pygame.font.SysFont("monospace", 30) - textSurf, textRect = text_objects(msg, smallText, white) - textRect.center = ((x + (w / 2)), (y + (h / 2))) - screen.blit(textSurf, textRect) + small_text = pygame.font.SysFont("monospace", 30) + text_surf, text_rect = text_objects(msg, small_text, white) + text_rect.center = ((x + (w / 2)), (y + (h / 2))) + screen.blit(text_surf, text_rect) button("PLAY!", 150, 450, 100, 50, white, white, start) button("PLAY", 152, 452, 96, 46, black, black, start) diff --git a/game_AI_vs_AI.py b/game_AI_vs_AI.py new file mode 100644 index 0000000..6d23665 --- /dev/null +++ b/game_AI_vs_AI.py @@ -0,0 +1,105 @@ +import sys + +import pygame + +from config import black, white +from connect_game import ConnectGame +from game_data import GameData +from game_renderer import GameRenderer + +from agents import RandomAgent +from random import choice +from time import sleep + + +def quit(): + sys.exit() + + +def start(): + agent1 = RandomAgent() + agent2 = RandomAgent() + + delay = 1 + data = GameData() + screen = pygame.display.set_mode(data.size) + game = ConnectGame(data, GameRenderer(screen, data)) + + game.print_board() + game.draw() + + pygame.display.update() + pygame.time.wait(1000) + + agent1_turn = choice([0, 1]) + + # Processes mouse and keyboard events, dispatching events to the event bus. + # The events are handled by the ConnectGame and GameRenderer classes. + while not game.game_data.game_over: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + game.quit() + + sleep(delay) + if data.turn == agent1_turn and not game.game_data.game_over: + game.do_movement(agent1.get_move(data)) + game.update() + game.draw() + else: + game.do_movement(agent2.get_move(data)) + game.update() + game.draw() + + game.update() + game.draw() + + +def text_objects(text, font, color): + textSurface = font.render(text, True, color) + return textSurface, textSurface.get_rect() + + +def message_display(text, color, p, q, v): + largeText = pygame.font.SysFont("monospace", v) + TextSurf, TextRect = text_objects(text, largeText, color) + TextRect.center = (p, q) + screen.blit(TextSurf, TextRect) + + +pygame.init() +screen = pygame.display.set_mode(GameData().size) +pygame.display.set_caption("Connect Four | Mayank Singh") +message_display("CONNECT FOUR!!", white, 350, 150, 75) +message_display("HAVE FUN!", (23, 196, 243), 350, 300, 75) + +running = True +while running: + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + + def button(msg, x, y, w, h, ic, ac, action=None): + mouse = pygame.mouse.get_pos() + click = pygame.mouse.get_pressed() + + if x + w > mouse[0] > x and y + h > mouse[1] > y: + pygame.draw.rect(screen, ac, (x, y, w, h)) + + if click[0] == 1 and action is not None: + action() + else: + pygame.draw.rect(screen, ic, (x, y, w, h)) + + smallText = pygame.font.SysFont("monospace", 30) + textSurf, textRect = text_objects(msg, smallText, white) + textRect.center = ((x + (w / 2)), (y + (h / 2))) + screen.blit(textSurf, textRect) + + + button("PLAY!", 150, 450, 100, 50, white, white, start) + button("PLAY", 152, 452, 96, 46, black, black, start) + button("QUIT", 450, 450, 100, 50, white, white, quit) + button("QUIT", 452, 452, 96, 46, black, black, quit) + pygame.display.update() diff --git a/game_board.py b/game_board.py index 73a4cd5..0ae7b68 100644 --- a/game_board.py +++ b/game_board.py @@ -1,5 +1,6 @@ from numpy import flip, zeros -from numpy.core._multiarray_umath import ndarray +from numpy import ndarray +from typing import Set, Tuple class GameBoard: @@ -12,15 +13,25 @@ class GameBoard: cols: int rows: int + # Class variables to reduce the number of operations by remembering previous calculus + # (specially useful for the AI agents): + slots_filled: int + # The squares where if a player had a piece would win the game + p1_win_squares: Set[Tuple[int, int]] # {(row, col), ...} + p2_win_squares: Set[Tuple[int, int]] + def __init__(self, rows=6, cols=7): """ Initializes the game board. :param rows: The height of the board in rows. - :param cols: The width of the boarrd in columns. + :param cols: The width of the board in columns. """ self.rows = rows self.cols = cols self.board = zeros((rows, cols)) + self.slots_filled = 0 + self.p1_win_squares = set() + self.p2_win_squares = set() def print_board(self): """ @@ -32,12 +43,25 @@ def print_board(self): def drop_piece(self, row, col, piece): """ - Drops a piece into the slot at position (row, col) + Drops a piece into the slot at position (row, col). + It also delete from pX_win_squares a coordinate if a enemy piece is placed. :param row: The row of the slot. :param col: The column of the slot. :param piece: The piece to drop. """ self.board[row][col] = piece + self.slots_filled += 1 + + coord = (row, col) + if piece == 1: + if coord in self.p2_win_squares: + self.p2_win_squares.remove(coord) + elif piece == 2: + if coord in self.p1_win_squares: + self.p1_win_squares.remove(coord) + + print("P1:", self.p1_win_squares) + print("P2:", self.p2_win_squares) def is_valid_location(self, col): """ @@ -57,12 +81,10 @@ def get_next_open_row(self, col): if self.board[row][col] == 0: return row - def check_square(self, piece, r, c): + def is_valid_coord(self, r, c) -> bool: """ - Checks if a particular square is a certain color. If - the space is off of the board it returns False. + If the space is off of the board it returns False. - :param piece: The piece color to look for. :param r: The row to check. :param c: The column to check. :return: Whether the square is on the board and has the color/piece specified. @@ -73,9 +95,24 @@ def check_square(self, piece, r, c): if c < 0 or c >= self.cols: return False + return True + + def check_square(self, piece, r, c) -> bool: + """ + Checks if a particular square is a certain color. If + the space is off of the board it returns False. + + :param piece: The piece color to look for. + :param r: The row to check. + :param c: The column to check. + :return: Whether the square is on the board and has the color/piece specified. + """ + if not self.is_valid_coord(r, c): + return False + return self.board[r][c] == piece - def horizontal_win(self, piece, r, c): + def horizontal_win(self, piece, r, c) -> bool: """ Checks if there is a horizontal win at the position (r,c) :param piece: The color of the chip to check for. @@ -83,14 +120,18 @@ def horizontal_win(self, piece, r, c): :param c: The column. :return: Whether there is a horizontal win at the position (r, c). """ - return ( - self.check_square(piece, r, c) - and self.check_square(piece, r, c + 1) - and self.check_square(piece, r, c + 2) - and self.check_square(piece, r, c + 3) - ) + consecutive_pieces = 0 + for c in range(c - 3, c + 4): + if self.check_square(piece, r, c): + consecutive_pieces += 1 + if consecutive_pieces == 4: + return True + else: + consecutive_pieces = 0 + + return False - def vertical_win(self, piece, r, c): + def vertical_win(self, piece, r, c) -> bool: """ Checks if there is vertical win at the position (r, c) :param piece: The color of the chip to check for. @@ -98,12 +139,12 @@ def vertical_win(self, piece, r, c): :param c: The column :return: Whether there is a vertical win at the position (r, c) """ - return ( - self.check_square(piece, r, c) - and self.check_square(piece, r + 1, c) - and self.check_square(piece, r + 2, c) - and self.check_square(piece, r + 3, c) - ) + consecutive_pieces = 0 + for r in range(r - 3, r + 1): + if self.check_square(piece, r, c): + consecutive_pieces += 1 + + return consecutive_pieces == 4 def diagonal_win(self, piece, r, c): """ @@ -113,44 +154,126 @@ def diagonal_win(self, piece, r, c): :param c: The column :return: Whether there is a diagonal win at the position (r,c) """ - return ( - self.check_square(piece, r, c) - and self.check_square(piece, r + 1, c + 1) - and self.check_square(piece, r + 2, c + 2) - and self.check_square(piece, r + 3, c + 3) - ) or ( - self.check_square(piece, r, c) - and self.check_square(piece, r - 1, c + 1) - and self.check_square(piece, r - 2, c + 2) - and self.check_square(piece, r - 3, c + 3) - ) + consecutive_pieces = 0 + for r_1, c_1 in zip(range(r - 3, r + 4), range(c + 3, c - 4, -1)): + if self.check_square(piece, r_1, c_1): + consecutive_pieces += 1 + if consecutive_pieces == 4: + return True + else: + consecutive_pieces = 0 + + consecutive_pieces = 0 + for r_2, c_2 in zip(range(r - 3, r + 4), range(c - 3, c + 4)): + if self.check_square(piece, r_2, c_2): + consecutive_pieces += 1 + if consecutive_pieces == 4: + return True + else: + consecutive_pieces = 0 + + return False + + def _set_horizontal_win_squares(self, piece, r, c): + """ + Add all the win squares placed in a horizontal direction respect a given coordinate (r, c) + :param piece: The color of the chip to check for. + :param r: The row. + :param c: The column. + :return: Whether there is a horizontal win at the position (r, c). + """ + for c in range(c - 3, c + 4): + self._set_win_square(piece, r, c, "h") + + def _set_vertical_win_squares(self, piece, r, c): + """ + Add all the win squares placed in a vertical direction respect a given coordinate (r, c) + :param piece: The color of the chip to check for. + :param r: The row + :param c: The column + :return: Whether there is a vertical win at the position (r, c) + """ + for r in range(r - 3, r + 4): + self._set_win_square(piece, r, c, "v") + + def _set_diagonal_win_squares(self, piece, r, c): + """ + Add all the win squares placed in a diagonal direction respect a given coordinate (r, c) + :param piece: The color of the chip to check for. + :param r: The row + :param c: The column + :return: Whether there is a diagonal win at the position (r,c) + """ + for r_1, c_1 in zip(range(r - 3, r + 4), range(c + 3, c - 4, -1)): + self._set_win_square(piece, r_1, c_1, "d") + + for r_2, c_2 in zip(range(r - 3, r + 4), range(c - 3, c + 4)): + self._set_win_square(piece, r_2, c_2, "d") - def winning_move(self, piece): + def _set_win_square(self, piece, r, c, direction: str): + """ + Add the given coordinates in the correspondent attribute + (p1_win_squares or p2_win_squares) if the correspond square is a + win square. + :param piece: The color of the chip to check for. + :param r: The row + :param c: The column + :param direction: if it is [v]ertical, [d]iagonal or [h]orizontal + """ + if self.is_valid_coord(r, c): + if self.board[r][c] == 0: + self.board[r][c] = piece + + if direction == "v": + check = self.vertical_win(piece, r, c) + elif direction == "d": + check = self.diagonal_win(piece, r, c) + elif direction == "h": + check = self.horizontal_win(piece, r, c) + else: + raise ValueError("Parameter 'direction' must be: 'h', 'v' or 'd'") + + if piece == 1 and check: + self.p1_win_squares.add((r, c)) + elif piece == 2 and check: + self.p2_win_squares.add((r, c)) + + self.board[r][c] = 0 + + def _analyze_square(self, piece, r, c): + """ + This function find ALL the possible winning squares + surround the given coordinates and add them to the correspond attribute + (p1_win_squares or p2_win_squares) + :param piece: The color of the chip to check for. + :param r: The row + :param c: The column + """ + self._set_horizontal_win_squares(piece, r, c) + self._set_vertical_win_squares(piece, r, c) + self._set_diagonal_win_squares(piece, r, c) + + def winning_move(self, piece, r, c) -> bool: """ Checks if the current piece has won the game. + It also calls to _analyze_square(). :param piece: The color of the chip to check for. + :param r: The row + :param c: The column :return: Whether the current piece has won the game. """ - for c in range(self.cols): - for r in range(self.rows): - if ( - self.horizontal_win(piece, r, c) - or self.vertical_win(piece, r, c) - or self.diagonal_win(piece, r, c) - ): - return True - return False + self._analyze_square(piece, r, c) + if piece == 1: + return (r, c) in self.p1_win_squares + + return (r, c) in self.p2_win_squares def tie_move(self): """ Checks for a tie game. :return: Whether a tie has occurred. """ - slots_filled: int = 0 - - for c in range(self.cols): - for r in range(self.rows): - if self.board[r][c] != 0: - slots_filled += 1 + return self.slots_filled == (self.rows * self.cols) - return slots_filled == 42 + def __str__(self): + return str(self.board) diff --git a/game_data.py b/game_data.py index a7ae2fc..640aab3 100644 --- a/game_data.py +++ b/game_data.py @@ -13,6 +13,7 @@ class GameData: width: int sq_size: int size: Tuple[int, int] + game_over: bool turn: int last_move_row: [int] diff --git a/game_human_vs_AI.py b/game_human_vs_AI.py new file mode 100644 index 0000000..1192d16 --- /dev/null +++ b/game_human_vs_AI.py @@ -0,0 +1,110 @@ +import sys + +import pygame +from pygame.locals import KEYDOWN + +from config import black, white +from connect_game import ConnectGame +from events import MouseClickEvent, MouseHoverEvent, bus +from game_data import GameData +from game_renderer import GameRenderer + +from agents import RandomAgent +from random import choice + + +def quit(): + sys.exit() + + +def start(): + agent = RandomAgent() + data = GameData() + screen = pygame.display.set_mode(data.size) + game = ConnectGame(data, GameRenderer(screen, data)) + + game.print_board() + game.draw() + + pygame.display.update() + pygame.time.wait(1000) + + agent_turn = choice([0, 1]) + + # Processes mouse and keyboard events, dispatching events to the event bus. + # The events are handled by the ConnectGame and GameRenderer classes. + while not game.game_data.game_over: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + game.quit() + + if event.type == pygame.MOUSEMOTION: + bus.emit("mouse:hover", game.renderer, MouseHoverEvent(event.pos[0])) + + if event.type == pygame.MOUSEBUTTONDOWN: + bus.emit("mouse:click", game, MouseClickEvent(event.pos[0])) + + if event.type == KEYDOWN: + if event.key == pygame.K_z: + mods: int = pygame.key.get_mods() + if mods & pygame.KMOD_CTRL: + bus.emit("game:undo", game) + + if data.turn == agent_turn and not game.game_data.game_over: + game.do_movement(agent.get_move(data)) + game.update() + game.draw() + + game.update() + game.draw() + + +def text_objects(text, font, color): + textSurface = font.render(text, True, color) + return textSurface, textSurface.get_rect() + + +def message_display(text, color, p, q, v): + largeText = pygame.font.SysFont("monospace", v) + TextSurf, TextRect = text_objects(text, largeText, color) + TextRect.center = (p, q) + screen.blit(TextSurf, TextRect) + + +pygame.init() +screen = pygame.display.set_mode(GameData().size) +pygame.display.set_caption("Connect Four | Mayank Singh") +message_display("CONNECT FOUR!!", white, 350, 150, 75) +message_display("HAVE FUN!", (23, 196, 243), 350, 300, 75) + +running = True +while running: + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + + def button(msg, x, y, w, h, ic, ac, action=None): + mouse = pygame.mouse.get_pos() + click = pygame.mouse.get_pressed() + + if x + w > mouse[0] > x and y + h > mouse[1] > y: + pygame.draw.rect(screen, ac, (x, y, w, h)) + + if click[0] == 1 and action is not None: + action() + else: + pygame.draw.rect(screen, ic, (x, y, w, h)) + + smallText = pygame.font.SysFont("monospace", 30) + textSurf, textRect = text_objects(msg, smallText, white) + textRect.center = ((x + (w / 2)), (y + (h / 2))) + screen.blit(textSurf, textRect) + + + button("PLAY!", 150, 450, 100, 50, white, white, start) + button("PLAY", 152, 452, 96, 46, black, black, start) + button("QUIT", 450, 450, 100, 50, white, white, quit) + button("QUIT", 452, 452, 96, 46, black, black, quit) + pygame.display.update() From 4c287c5585f6908ca9d313bd7be856ae1e37c33c Mon Sep 17 00:00:00 2001 From: Pablo Date: Fri, 19 Mar 2021 13:12:05 +0100 Subject: [PATCH 2/4] AI based on minimax algorithm and other improvements The MinimaxAgent uses also the alpha beta pruning to improve the speed. I have also made some changes to have a cleaner code in other files and some improvements in the compare_agent method of ConnectGame. --- agents.py | 218 +++++++++++++++++++++++++++++++++++++++++--- connect_game.py | 120 +++++++++++++++--------- game_AI_vs_AI.py | 37 ++++---- game_board.py | 46 +++++----- game_data.py | 2 + game_human_vs_AI.py | 26 +++--- 6 files changed, 339 insertions(+), 110 deletions(-) diff --git a/agents.py b/agents.py index 86b5abe..b6ddaae 100644 --- a/agents.py +++ b/agents.py @@ -1,24 +1,214 @@ from random import choice +from copy import deepcopy from game_data import GameData import abc class Agent(abc.ABC): - """ - It is an abstract class. All the agents should inheritance from this class. - """ + """ + It is an abstract class. All the agents must inherit from this class. + """ - @staticmethod - def get_move(game_data: GameData) -> int: - pass + def get_move(self, game_data: GameData) -> int: + pass class RandomAgent(Agent): - """ - An agent which makes random moves - """ - - @staticmethod - def get_move(data) -> int: - """ returns a random valid col""" - return choice([c for c in range(7) if data.game_board.is_valid_location(c)]) + """ + An agent which makes random moves. + """ + + @staticmethod + def get_move(data) -> int: + """ returns a random valid col""" + return choice([c for c in range(7) if data.game_board.is_valid_location(c)]) + + +class MinimaxAgent(Agent): + """ + An agent designed to play connect-4 in a 6x7 board. + It uses the minimax algorithm and alpha-beta pruning with a recommend depth of 5 moves. + The heuristic it is based on the Odd-Even strategy and it is 100% original. + """ + __depth: int + + def __init__(self, depth=5): + self.__depth = depth + + @staticmethod + def get_board_value(game_data: GameData) -> int: + + """ + We calculate this value with the following heuristic: + - Squares value: + [[0 0 0 0 0 0 0] + [0 0 1 2 1 0 0] + [0 0 1 2 1 0 0] + [0 0 2 2 2 0 0] + [0 0 2 2 2 0 0] + [0 0 0 2 0 0 0]] + + - We also take into account the win squares of each player and where they are located. + This heuristic is mainly based on the Odd-Even strategy. + More info about this strategy in: https://www.youtube.com/watch?v=YqqcNjQMX18 + + :param game_data: All of the data for the game. + :returns: The value of the board. Positive positions are good for player 1 + while negative ones indicate a better position for player 2. + """ + if game_data.game_board.slots_filled == 0: + return 0 + + if game_data.winner == 1: + return 1000 + elif game_data.winner == 2: + return -1000 + + total_value = 0 + + # Setting points per piece position: + for row in game_data.game_board.board[3:5, 2:5:2]: + for chip in row: + if chip == 1: + total_value += 1 + elif chip == 2: + total_value -= 1 + + for row in game_data.game_board.board[1:3, 2:5:2]: + for chip in row: + if chip == 1: + total_value += 2 + elif chip == 2: + total_value -= 2 + + for chip in game_data.game_board.board[:5, 3]: + if chip == 1: + total_value += 2 + elif chip == 2: + total_value -= 2 + + # Setting points per win square (Odd-Even strategy): + p1_win_odd_rows_per_column = \ + [[r for r, c in game_data.game_board.p1_win_squares if c == i and r % 2 == 0] for i in range(7)] + p2_win_odd_rows_per_column = \ + [[r for r, c in game_data.game_board.p2_win_squares if c == i and r % 2 == 0] for i in range(7)] + + p1_win_even_rows_per_column = \ + [[r for r, c in game_data.game_board.p1_win_squares if c == i and r % 2 == 1] for i in range(7)] + p2_win_even_rows_per_column = \ + [[r for r, c in game_data.game_board.p2_win_squares if c == i and r % 2 == 1] for i in range(7)] + + for c in range(7): + # Column winners: + p1_best_even_row = 10 if not p1_win_even_rows_per_column[c] else min(p1_win_even_rows_per_column[c]) + p2_best_even_row = 10 if not p2_win_even_rows_per_column[c] else min(p2_win_even_rows_per_column[c]) + + p2_best_odd_row = 10 if not p2_win_odd_rows_per_column[c] else min(p2_win_odd_rows_per_column[c]) + p1_best_odd_row = 10 if not p1_win_odd_rows_per_column[c] else min(p1_win_odd_rows_per_column[c]) + + if p1_best_odd_row < p2_best_even_row: + total_value += 100 - p1_best_odd_row * 5 + + if p2_best_odd_row < p1_best_even_row: + total_value -= 10 - p2_best_odd_row + + if p2_best_even_row < p1_best_odd_row: + total_value -= 50 - p2_best_even_row * 3 + + return total_value + + @staticmethod + def __drop_piece(data, row, col, piece): + """ + Drops a piece (it should be in a copy board, not in the original data) + and updates all the necessary information. + """ + data.game_board.drop_piece(row, col, piece) + data.turn += 1 + data.turn %= 2 + if data.game_board.winning_move(piece, row, col): + data.game_over = True + data.winner = piece + + @staticmethod + def _alpha_beta(data: GameData, col: int, depth=5, alpha=-1001, beta=1001) -> int: + """ + :return: The value of a given movement. + """ + + # making the move in a copy of the real board: + data_copy = deepcopy(data) + piece = data_copy.turn + 1 + row = data_copy.game_board.get_next_open_row(col) + MinimaxAgent.__drop_piece(data_copy, row, col, piece) + + if depth == 0 or data_copy.game_over: + return MinimaxAgent.get_board_value(data_copy) + + if data_copy.turn == 0: + + max_value = -1001 + valid_moves = [c for c in range(7) if data_copy.game_board.is_valid_location(c)] + # Looking for center moves first in order to find the best move faster: + center_moves = valid_moves[len(valid_moves) // 3:] + other_moves = valid_moves[:len(valid_moves) // 3] + + for col in center_moves: + + move_value = MinimaxAgent._alpha_beta(data_copy, col, depth - 1, alpha, beta) + max_value = max(max_value, move_value) + alpha = max(alpha, move_value) + if beta <= alpha: + return max_value + + for col in other_moves: + + move_value = MinimaxAgent._alpha_beta(data_copy, col, depth - 1, alpha, beta) + max_value = max(max_value, move_value) + alpha = max(alpha, move_value) + if beta <= alpha: + return max_value + + return max_value + + else: + + min_value = 1001 + valid_moves = [col for col in range(7) if data_copy.game_board.is_valid_location(col)] + # Look for center moves first: + center_moves = valid_moves[len(valid_moves) // 3:] + other_moves = valid_moves[:len(valid_moves) // 3] + for col in center_moves: + move_value = MinimaxAgent._alpha_beta(data_copy, col, depth - 1, alpha, beta) + min_value = min(min_value, move_value) + beta = min(beta, move_value) + if beta <= alpha: + return min_value + + for col in other_moves: + move_value = MinimaxAgent._alpha_beta(data_copy, col, depth - 1, alpha, beta) + min_value = min(min_value, move_value) + alpha = min(alpha, move_value) + if beta <= alpha: + return min_value + + return min_value + + def get_move(self, game_data: GameData) -> int: + """ + This is the method that have to be called in order to get the move of our MiniMax agent. + :param game_data: All of the data for the game. + :return: The chosen col. + """ + + if game_data.game_board.slots_filled == 0: + return 3 + possible_moves = [col for col in range(7) if game_data.game_board.is_valid_location(col)] + move_values = [MinimaxAgent._alpha_beta(game_data, move, self.__depth) for move in possible_moves] + + if game_data.turn == 0: + best_moves = [i for i in possible_moves if move_values[possible_moves.index(i)] == max(move_values)] + else: + best_moves = [i for i in possible_moves if move_values[possible_moves.index(i)] == min(move_values)] + + return choice(best_moves) diff --git a/connect_game.py b/connect_game.py index b9aacf8..0236e22 100644 --- a/connect_game.py +++ b/connect_game.py @@ -3,13 +3,13 @@ import sys import pygame +from typing import List +from random import choice from config import black from events import GameOver, MouseClickEvent, PieceDropEvent, bus from game_data import GameData from game_renderer import GameRenderer - -from typing import List from agents import Agent @@ -50,14 +50,13 @@ def mouse_click(self, event: MouseClickEvent): col: int = int(math.floor(event.posx / self.game_data.sq_size)) - self.do_movement(col) + self.make_movement(col) @bus.on("game:undo") def undo(self): """ Handles the Ctrl+Z keyboard sequence, which is used to roll back the last move. - :return: """ if self.game_data.last_move_row: @@ -68,10 +67,10 @@ def undo(self): self.game_data.turn += 1 self.game_data.turn = self.game_data.turn % 2 - def do_movement(self, col: int): + def make_movement(self, col: int): """ Allows to make a movement without a mouse click. - Inserts a new piece in the specified column and prints the new board + Inserts a new piece in the specified column and prints the new board. """ if self.game_data.game_board.is_valid_location(col): row: int = self.game_data.game_board.get_next_open_row(col) @@ -100,62 +99,99 @@ def do_movement(self, col: int): self.game_data.turn = self.game_data.turn % 2 @staticmethod - def compare_agents(agent1: Agent, agent2: Agent, n=100, alternate=True) -> List[int]: + def play_game(player1: Agent, player2: Agent) -> int: + """ + Agent1 plays first, agent2 plays second + :param player1: an AI agent + :param player2: an AI agent + :returns: the winner; 1 = agent1, 2 = agent2, 0 = tie + """ + data = GameData() + board = data.game_board + while True: + col = player1.get_move(data) + row = board.get_next_open_row(col) + board.drop_piece(row, col, 1) + if board.winning_move(1, row, col): + return 1 + + data.turn += 1 + data.turn = data.turn % 2 + + col = player2.get_move(data) + row = board.get_next_open_row(col) + board.drop_piece(row, col, 2) + + if board.winning_move(2, row, col): + return 2 + + if board.tie_move(): + return 0 + + @staticmethod + def compare_agents(agent1: Agent, agent2: Agent, n=5, alternate=True, print_progress=True) -> List[int]: """ The 2 given agents will play between them n times. The games are not showed. :param agent1: an AI agent :param agent2: an AI agent :param n: number of matches - :param alternate: if True agent1 and agent2 will play first the same number of times - :returns: number of [ties, agent1 wins, agent2 wins] - """ - - def play_game(agent1: Agent, agent2: Agent) -> int: - """ - Agent1 plays first, agent2 plays second - :param agent1: an AI agent - :param agent2: an AI agent - :returns: the winner; 1 = agent1, 2 = agent2, 0 = tie - """ - data = GameData() - board = data.game_board - while True: - col = agent1.get_move(data) - row = board.get_next_open_row(col) - board.drop_piece(row, col, 1) - if board.winning_move(1, row, col): - return 1 - - data.turn += 1 - data.turn = data.turn % 2 - - col = agent2.get_move(data) - row = board.get_next_open_row(col) - board.drop_piece(row, col, 2) - - if board.winning_move(2, row, col): - return 2 - - if board.tie_move(): - return 0 + :param alternate: if True player1 and player2 will play first the same number of times + :returns: number of [ties, player1 wins, player2 wins] + """ stats = [0, 0, 0] + completed_games = 0 if alternate: + if n % 2 != 0: + if choice([1, 2]) == 1: + winner = ConnectGame.play_game(agent1, agent2) + stats[winner] += 1 + completed_games += 1 + if print_progress: + print(f"finished games: {completed_games}/{n}") + print("current stats:", stats) + else: + winner = ConnectGame.play_game(agent2, agent1) + completed_games += 1 + if winner == 1: + stats[2] += 1 + elif winner == 2: + stats[1] += 1 + else: + stats[0] += 1 + + if print_progress: + print(f"finished games: {completed_games}/{n}") + print("current stats:", stats) + for _ in range(n // 2): - winner = play_game(agent1, agent2) + winner = ConnectGame.play_game(agent1, agent2) stats[winner] += 1 + completed_games += 1 + if print_progress: + print(f"finished games: {completed_games}/{n}") + print("current stats:", stats) - winner = play_game(agent2, agent1) + winner = ConnectGame.play_game(agent2, agent1) + completed_games += 1 if winner == 1: stats[2] += 1 elif winner == 2: stats[1] += 1 else: stats[0] += 1 + + if print_progress: + print(f"finished games: {completed_games}/{n}") + print("current stats:", stats) else: for _ in range(n): - winner = play_game(agent1, agent2) + winner = ConnectGame.play_game(agent1, agent2) + completed_games += 1 stats[winner] += 1 + if print_progress: + print(f"finished games: {completed_games}/{n}") + print("current stats:", stats) return stats diff --git a/game_AI_vs_AI.py b/game_AI_vs_AI.py index 6d23665..2dd7df4 100644 --- a/game_AI_vs_AI.py +++ b/game_AI_vs_AI.py @@ -7,8 +7,7 @@ from game_data import GameData from game_renderer import GameRenderer -from agents import RandomAgent -from random import choice +from agents import MinimaxAgent, RandomAgent from time import sleep @@ -17,10 +16,10 @@ def quit(): def start(): - agent1 = RandomAgent() - agent2 = RandomAgent() + agent1 = MinimaxAgent() # red + agent2 = RandomAgent() # yellow - delay = 1 + delay = 0.5 data = GameData() screen = pygame.display.set_mode(data.size) game = ConnectGame(data, GameRenderer(screen, data)) @@ -29,9 +28,9 @@ def start(): game.draw() pygame.display.update() - pygame.time.wait(1000) + pygame.time.wait(10) - agent1_turn = choice([0, 1]) + agent1_turn = 0 # Processes mouse and keyboard events, dispatching events to the event bus. # The events are handled by the ConnectGame and GameRenderer classes. @@ -42,11 +41,11 @@ def start(): sleep(delay) if data.turn == agent1_turn and not game.game_data.game_over: - game.do_movement(agent1.get_move(data)) + game.make_movement(agent1.get_move(data)) game.update() game.draw() else: - game.do_movement(agent2.get_move(data)) + game.make_movement(agent2.get_move(data)) game.update() game.draw() @@ -55,15 +54,15 @@ def start(): def text_objects(text, font, color): - textSurface = font.render(text, True, color) - return textSurface, textSurface.get_rect() + text_surface = font.render(text, True, color) + return text_surface, text_surface.get_rect() def message_display(text, color, p, q, v): - largeText = pygame.font.SysFont("monospace", v) - TextSurf, TextRect = text_objects(text, largeText, color) - TextRect.center = (p, q) - screen.blit(TextSurf, TextRect) + large_text = pygame.font.SysFont("monospace", v) + text_surf, text_rect = text_objects(text, large_text, color) + text_rect.center = (p, q) + screen.blit(text_surf, text_rect) pygame.init() @@ -92,10 +91,10 @@ def button(msg, x, y, w, h, ic, ac, action=None): else: pygame.draw.rect(screen, ic, (x, y, w, h)) - smallText = pygame.font.SysFont("monospace", 30) - textSurf, textRect = text_objects(msg, smallText, white) - textRect.center = ((x + (w / 2)), (y + (h / 2))) - screen.blit(textSurf, textRect) + small_text = pygame.font.SysFont("monospace", 30) + text_surf, text_rect = text_objects(msg, small_text, white) + text_rect.center = ((x + (w / 2)), (y + (h / 2))) + screen.blit(text_surf, text_rect) button("PLAY!", 150, 450, 100, 50, white, white, start) diff --git a/game_board.py b/game_board.py index 0ae7b68..0f308f3 100644 --- a/game_board.py +++ b/game_board.py @@ -13,12 +13,10 @@ class GameBoard: cols: int rows: int - # Class variables to reduce the number of operations by remembering previous calculus - # (specially useful for the AI agents): slots_filled: int - # The squares where if a player had a piece would win the game - p1_win_squares: Set[Tuple[int, int]] # {(row, col), ...} - p2_win_squares: Set[Tuple[int, int]] + + p1_win_squares: Set[Tuple[int, int]] # The squares where if a player had a piece would win the game + p2_win_squares: Set[Tuple[int, int]] # {(row, col), ...} def __init__(self, rows=6, cols=7): """ @@ -28,7 +26,7 @@ def __init__(self, rows=6, cols=7): """ self.rows = rows self.cols = cols - self.board = zeros((rows, cols)) + self.board = zeros((rows, cols), dtype=int) self.slots_filled = 0 self.p1_win_squares = set() self.p2_win_squares = set() @@ -49,10 +47,12 @@ def drop_piece(self, row, col, piece): :param col: The column of the slot. :param piece: The piece to drop. """ + assert isinstance(row, int) self.board[row][col] = piece self.slots_filled += 1 - + self._analyze_square(piece, row, col) coord = (row, col) + if piece == 1: if coord in self.p2_win_squares: self.p2_win_squares.remove(coord) @@ -60,9 +60,6 @@ def drop_piece(self, row, col, piece): if coord in self.p1_win_squares: self.p1_win_squares.remove(coord) - print("P1:", self.p1_win_squares) - print("P2:", self.p2_win_squares) - def is_valid_location(self, col): """ Returns whether the position exists on the board. @@ -77,6 +74,7 @@ def get_next_open_row(self, col): :param col: The column to check for a free space. :return: The next free row for a column. """ + assert self.rows > 0 for row in range(self.rows): if self.board[row][col] == 0: return row @@ -182,7 +180,9 @@ def _set_horizontal_win_squares(self, piece, r, c): :param c: The column. :return: Whether there is a horizontal win at the position (r, c). """ + assert isinstance(r, int) for c in range(c - 3, c + 4): + assert isinstance(r, int) self._set_win_square(piece, r, c, "h") def _set_vertical_win_squares(self, piece, r, c): @@ -212,7 +212,7 @@ def _set_diagonal_win_squares(self, piece, r, c): def _set_win_square(self, piece, r, c, direction: str): """ - Add the given coordinates in the correspondent attribute + Adds the given coordinates in the correspondent attribute (p1_win_squares or p2_win_squares) if the correspond square is a win square. :param piece: The color of the chip to check for. @@ -228,10 +228,8 @@ def _set_win_square(self, piece, r, c, direction: str): check = self.vertical_win(piece, r, c) elif direction == "d": check = self.diagonal_win(piece, r, c) - elif direction == "h": - check = self.horizontal_win(piece, r, c) else: - raise ValueError("Parameter 'direction' must be: 'h', 'v' or 'd'") + check = self.horizontal_win(piece, r, c) if piece == 1 and check: self.p1_win_squares.add((r, c)) @@ -242,8 +240,8 @@ def _set_win_square(self, piece, r, c, direction: str): def _analyze_square(self, piece, r, c): """ - This function find ALL the possible winning squares - surround the given coordinates and add them to the correspond attribute + This function find ALL the winning squares + surround the given coordinates and adds them to the correspond attribute (p1_win_squares or p2_win_squares) :param piece: The color of the chip to check for. :param r: The row @@ -256,17 +254,18 @@ def _analyze_square(self, piece, r, c): def winning_move(self, piece, r, c) -> bool: """ Checks if the current piece has won the game. - It also calls to _analyze_square(). :param piece: The color of the chip to check for. :param r: The row :param c: The column :return: Whether the current piece has won the game. """ - self._analyze_square(piece, r, c) - if piece == 1: - return (r, c) in self.p1_win_squares - return (r, c) in self.p2_win_squares + if piece == 1 and (r, c) in self.p1_win_squares: + return True + elif (r, c) in self.p2_win_squares: + return True + + return False def tie_move(self): """ @@ -276,4 +275,7 @@ def tie_move(self): return self.slots_filled == (self.rows * self.cols) def __str__(self): - return str(self.board) + return str(flip(self.board, 0)) + + def __iter__(self): + return iter(self.board) diff --git a/game_data.py b/game_data.py index 640aab3..a4cafa4 100644 --- a/game_data.py +++ b/game_data.py @@ -19,9 +19,11 @@ class GameData: last_move_row: [int] last_move_col: [int] game_board: GameBoard + winner: int # 0 = Nobody yet, 1 = player1, 2 = player2 def __init__(self): self.game_over = False + self.winner = 0 self.turn = 0 self.last_move_row = [] self.last_move_col = [] diff --git a/game_human_vs_AI.py b/game_human_vs_AI.py index 1192d16..d9f5d11 100644 --- a/game_human_vs_AI.py +++ b/game_human_vs_AI.py @@ -9,7 +9,7 @@ from game_data import GameData from game_renderer import GameRenderer -from agents import RandomAgent +from agents import MinimaxAgent from random import choice @@ -18,7 +18,7 @@ def quit(): def start(): - agent = RandomAgent() + agent = MinimaxAgent() data = GameData() screen = pygame.display.set_mode(data.size) game = ConnectGame(data, GameRenderer(screen, data)) @@ -51,7 +51,7 @@ def start(): bus.emit("game:undo", game) if data.turn == agent_turn and not game.game_data.game_over: - game.do_movement(agent.get_move(data)) + game.make_movement(agent.get_move(data)) game.update() game.draw() @@ -60,15 +60,15 @@ def start(): def text_objects(text, font, color): - textSurface = font.render(text, True, color) - return textSurface, textSurface.get_rect() + text_surface = font.render(text, True, color) + return text_surface, text_surface.get_rect() def message_display(text, color, p, q, v): - largeText = pygame.font.SysFont("monospace", v) - TextSurf, TextRect = text_objects(text, largeText, color) - TextRect.center = (p, q) - screen.blit(TextSurf, TextRect) + large_text = pygame.font.SysFont("monospace", v) + text_surf, text_rect = text_objects(text, large_text, color) + text_rect.center = (p, q) + screen.blit(text_surf, text_rect) pygame.init() @@ -97,10 +97,10 @@ def button(msg, x, y, w, h, ic, ac, action=None): else: pygame.draw.rect(screen, ic, (x, y, w, h)) - smallText = pygame.font.SysFont("monospace", 30) - textSurf, textRect = text_objects(msg, smallText, white) - textRect.center = ((x + (w / 2)), (y + (h / 2))) - screen.blit(textSurf, textRect) + small_text = pygame.font.SysFont("monospace", 30) + text_surf, text_rect = text_objects(msg, small_text, white) + text_rect.center = ((x + (w / 2)), (y + (h / 2))) + screen.blit(text_surf, text_rect) button("PLAY!", 150, 450, 100, 50, white, white, start) From eba346201e735fe5ad25fcc0fbf0d0978ec1534d Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 22 Jul 2021 13:20:58 +0200 Subject: [PATCH 3/4] button added to play vs AI --- game.py | 63 +++++++++++++++++++++++-- game_human_vs_AI.py | 110 -------------------------------------------- 2 files changed, 58 insertions(+), 115 deletions(-) delete mode 100644 game_human_vs_AI.py diff --git a/game.py b/game.py index acdfb2c..ddf0842 100644 --- a/game.py +++ b/game.py @@ -3,18 +3,21 @@ import pygame from pygame.locals import KEYDOWN +from random import choice + from config import black, white from connect_game import ConnectGame from events import MouseClickEvent, MouseHoverEvent, bus from game_data import GameData from game_renderer import GameRenderer +from agents import MinimaxAgent def quit(): sys.exit() -def start(): +def start_player_vs_player(): data = GameData() screen = pygame.display.set_mode(data.size) game = ConnectGame(data, GameRenderer(screen, data)) @@ -37,6 +40,43 @@ def start(): pygame.display.update() + if event.type == pygame.MOUSEBUTTONDOWN: + bus.emit("mouse:click", game, MouseClickEvent(event.pos[0])) + + if event.type == KEYDOWN: + if event.key == pygame.K_z: + mods = pygame.key.get_mods() + if mods & pygame.KMOD_CTRL: + bus.emit("game:undo", game) + + game.update() + game.draw() + + +def start_player_vs_ai(): + agent = MinimaxAgent() + data = GameData() + screen = pygame.display.set_mode(data.size) + game = ConnectGame(data, GameRenderer(screen, data)) + + game.print_board() + game.draw() + + pygame.display.update() + pygame.time.wait(1000) + + agent_turn = choice([0, 1]) + + # Processes mouse and keyboard events, dispatching events to the event bus. + # The events are handled by the ConnectGame and GameRenderer classes. + while not game.game_data.game_over: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + game.quit() + + if event.type == pygame.MOUSEMOTION: + bus.emit("mouse:hover", game.renderer, MouseHoverEvent(event.pos[0])) + if event.type == pygame.MOUSEBUTTONDOWN: bus.emit("mouse:click", game, MouseClickEvent(event.pos[0])) @@ -46,6 +86,11 @@ def start(): if mods & pygame.KMOD_CTRL: bus.emit("game:undo", game) + if data.turn == agent_turn and not game.game_data.game_over: + game.make_movement(agent.get_move(data)) + game.update() + game.draw() + game.update() game.draw() @@ -92,8 +137,16 @@ def button(msg, x, y, w, h, ic, ac, action=None): text_rect.center = ((x + (w / 2)), (y + (h / 2))) screen.blit(text_surf, text_rect) - button("PLAY!", 150, 450, 100, 50, white, white, start) - button("PLAY", 152, 452, 96, 46, black, black, start) - button("QUIT", 450, 450, 100, 50, white, white, quit) - button("QUIT", 452, 452, 96, 46, black, black, quit) + button("2 PLAYERS", 125, 450, 170, 50, white, white, start_player_vs_player) + button("2 PLAYERS", 127, 452, 166, 46, black, black, start_player_vs_player) + + # button("AI", 300, 450, 100, 50, white, white, start_player_vs_ai) + # button("AI", 302, 452, 96, 46, black, black, start_player_vs_ai) + + button("COMPUTER", 125, 510, 170, 50, white, white, start_player_vs_ai) + button("AI", 127, 512, 166, 46, black, black, start_player_vs_ai) + + button("QUIT", 500, 450, 100, 50, white, white, quit) + button("QUIT", 502, 452, 96, 46, black, black, quit) + pygame.display.update() diff --git a/game_human_vs_AI.py b/game_human_vs_AI.py deleted file mode 100644 index d9f5d11..0000000 --- a/game_human_vs_AI.py +++ /dev/null @@ -1,110 +0,0 @@ -import sys - -import pygame -from pygame.locals import KEYDOWN - -from config import black, white -from connect_game import ConnectGame -from events import MouseClickEvent, MouseHoverEvent, bus -from game_data import GameData -from game_renderer import GameRenderer - -from agents import MinimaxAgent -from random import choice - - -def quit(): - sys.exit() - - -def start(): - agent = MinimaxAgent() - data = GameData() - screen = pygame.display.set_mode(data.size) - game = ConnectGame(data, GameRenderer(screen, data)) - - game.print_board() - game.draw() - - pygame.display.update() - pygame.time.wait(1000) - - agent_turn = choice([0, 1]) - - # Processes mouse and keyboard events, dispatching events to the event bus. - # The events are handled by the ConnectGame and GameRenderer classes. - while not game.game_data.game_over: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - game.quit() - - if event.type == pygame.MOUSEMOTION: - bus.emit("mouse:hover", game.renderer, MouseHoverEvent(event.pos[0])) - - if event.type == pygame.MOUSEBUTTONDOWN: - bus.emit("mouse:click", game, MouseClickEvent(event.pos[0])) - - if event.type == KEYDOWN: - if event.key == pygame.K_z: - mods: int = pygame.key.get_mods() - if mods & pygame.KMOD_CTRL: - bus.emit("game:undo", game) - - if data.turn == agent_turn and not game.game_data.game_over: - game.make_movement(agent.get_move(data)) - game.update() - game.draw() - - game.update() - game.draw() - - -def text_objects(text, font, color): - text_surface = font.render(text, True, color) - return text_surface, text_surface.get_rect() - - -def message_display(text, color, p, q, v): - large_text = pygame.font.SysFont("monospace", v) - text_surf, text_rect = text_objects(text, large_text, color) - text_rect.center = (p, q) - screen.blit(text_surf, text_rect) - - -pygame.init() -screen = pygame.display.set_mode(GameData().size) -pygame.display.set_caption("Connect Four | Mayank Singh") -message_display("CONNECT FOUR!!", white, 350, 150, 75) -message_display("HAVE FUN!", (23, 196, 243), 350, 300, 75) - -running = True -while running: - - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - - - def button(msg, x, y, w, h, ic, ac, action=None): - mouse = pygame.mouse.get_pos() - click = pygame.mouse.get_pressed() - - if x + w > mouse[0] > x and y + h > mouse[1] > y: - pygame.draw.rect(screen, ac, (x, y, w, h)) - - if click[0] == 1 and action is not None: - action() - else: - pygame.draw.rect(screen, ic, (x, y, w, h)) - - small_text = pygame.font.SysFont("monospace", 30) - text_surf, text_rect = text_objects(msg, small_text, white) - text_rect.center = ((x + (w / 2)), (y + (h / 2))) - screen.blit(text_surf, text_rect) - - - button("PLAY!", 150, 450, 100, 50, white, white, start) - button("PLAY", 152, 452, 96, 46, black, black, start) - button("QUIT", 450, 450, 100, 50, white, white, quit) - button("QUIT", 452, 452, 96, 46, black, black, quit) - pygame.display.update() From 6e14b5da1b888e70fdb844fe478c42deda5e072a Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 22 Jul 2021 13:27:33 +0200 Subject: [PATCH 4/4] main function added to improve performance --- game.py | 71 ++++++++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/game.py b/game.py index ddf0842..df90cd0 100644 --- a/game.py +++ b/game.py @@ -100,53 +100,58 @@ def text_objects(text, font, color): return text_surface, text_surface.get_rect() -def message_display(text, color, p, q, v): +def message_display(text, color, p, q, v, screen): large_text = pygame.font.SysFont("monospace", v) text_surf, text_rect = text_objects(text, large_text, color) text_rect.center = (p, q) screen.blit(text_surf, text_rect) -pygame.init() -screen = pygame.display.set_mode(GameData().size) -pygame.display.set_caption("Connect Four | Mayank Singh") -message_display("CONNECT FOUR!!", white, 350, 150, 75) -message_display("HAVE FUN!", (23, 196, 243), 350, 300, 75) +def main(): + pygame.init() + screen = pygame.display.set_mode(GameData().size) + pygame.display.set_caption("Connect Four | Mayank Singh") + message_display("CONNECT FOUR!!", white, 350, 150, 75, screen) + message_display("HAVE FUN!", (23, 196, 243), 350, 300, 75, screen) -running = True -while running: + running = True + while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False - def button(msg, x, y, w, h, ic, ac, action=None): - mouse = pygame.mouse.get_pos() - click = pygame.mouse.get_pressed() + def button(msg, x, y, w, h, ic, ac, action=None): + mouse = pygame.mouse.get_pos() + click = pygame.mouse.get_pressed() - if x + w > mouse[0] > x and y + h > mouse[1] > y: - pygame.draw.rect(screen, ac, (x, y, w, h)) + if x + w > mouse[0] > x and y + h > mouse[1] > y: + pygame.draw.rect(screen, ac, (x, y, w, h)) - if click[0] == 1 and action is not None: - action() - else: - pygame.draw.rect(screen, ic, (x, y, w, h)) + if click[0] == 1 and action is not None: + action() + else: + pygame.draw.rect(screen, ic, (x, y, w, h)) - small_text = pygame.font.SysFont("monospace", 30) - text_surf, text_rect = text_objects(msg, small_text, white) - text_rect.center = ((x + (w / 2)), (y + (h / 2))) - screen.blit(text_surf, text_rect) + small_text = pygame.font.SysFont("monospace", 30) + text_surf, text_rect = text_objects(msg, small_text, white) + text_rect.center = ((x + (w / 2)), (y + (h / 2))) + screen.blit(text_surf, text_rect) - button("2 PLAYERS", 125, 450, 170, 50, white, white, start_player_vs_player) - button("2 PLAYERS", 127, 452, 166, 46, black, black, start_player_vs_player) + button("2 PLAYERS", 125, 450, 170, 50, white, white, start_player_vs_player) + button("2 PLAYERS", 127, 452, 166, 46, black, black, start_player_vs_player) - # button("AI", 300, 450, 100, 50, white, white, start_player_vs_ai) - # button("AI", 302, 452, 96, 46, black, black, start_player_vs_ai) + # button("AI", 300, 450, 100, 50, white, white, start_player_vs_ai) + # button("AI", 302, 452, 96, 46, black, black, start_player_vs_ai) - button("COMPUTER", 125, 510, 170, 50, white, white, start_player_vs_ai) - button("AI", 127, 512, 166, 46, black, black, start_player_vs_ai) + button("COMPUTER", 125, 510, 170, 50, white, white, start_player_vs_ai) + button("AI", 127, 512, 166, 46, black, black, start_player_vs_ai) - button("QUIT", 500, 450, 100, 50, white, white, quit) - button("QUIT", 502, 452, 96, 46, black, black, quit) + button("QUIT", 500, 450, 100, 50, white, white, quit) + button("QUIT", 502, 452, 96, 46, black, black, quit) - pygame.display.update() + pygame.display.update() + + +if __name__ == "__main__": + main() \ No newline at end of file