diff --git a/agents.py b/agents.py new file mode 100644 index 0000000..b6ddaae --- /dev/null +++ b/agents.py @@ -0,0 +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 must inherit from this class. + """ + + 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)]) + + +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 c5fe4a9..0236e22 100644 --- a/connect_game.py +++ b/connect_game.py @@ -3,11 +3,14 @@ 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 agents import Agent class ConnectGame: @@ -47,6 +50,28 @@ def mouse_click(self, event: MouseClickEvent): col: int = int(math.floor(event.posx / self.game_data.sq_size)) + 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. + """ + 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 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. + """ if self.game_data.game_board.is_valid_location(col): row: int = self.game_data.game_board.get_next_open_row(col) @@ -62,7 +87,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 +98,102 @@ 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 play_game(player1: Agent, player2: Agent) -> int: """ - Handles the Ctrl+Z keyboard sequence, which - is used to roll back the last move. - :return: + 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 player1 and player2 will play first the same number of times + :returns: number of [ties, player1 wins, player2 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 + 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 = 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 = 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 = 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 def update(self): """ diff --git a/game.py b/game.py index 5f16d96..df90cd0 100644 --- a/game.py +++ b/game.py @@ -3,18 +3,21 @@ import pygame from pygame.locals import KEYDOWN -from config import black, blue, white +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,54 +86,72 @@ 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() 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, 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) -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) +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) -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: -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 != 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) - 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("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("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() + # 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() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/game_AI_vs_AI.py b/game_AI_vs_AI.py new file mode 100644 index 0000000..2dd7df4 --- /dev/null +++ b/game_AI_vs_AI.py @@ -0,0 +1,104 @@ +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 MinimaxAgent, RandomAgent +from time import sleep + + +def quit(): + sys.exit() + + +def start(): + agent1 = MinimaxAgent() # red + agent2 = RandomAgent() # yellow + + delay = 0.5 + 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(10) + + agent1_turn = 0 + + # 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.make_movement(agent1.get_move(data)) + game.update() + game.draw() + else: + game.make_movement(agent2.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() diff --git a/game_board.py b/game_board.py index 73a4cd5..0f308f3 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,23 @@ class GameBoard: cols: int rows: int + slots_filled: 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): """ 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.board = zeros((rows, cols), dtype=int) + self.slots_filled = 0 + self.p1_win_squares = set() + self.p2_win_squares = set() def print_board(self): """ @@ -32,12 +41,24 @@ 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. """ + 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) + elif piece == 2: + if coord in self.p1_win_squares: + self.p1_win_squares.remove(coord) def is_valid_location(self, col): """ @@ -53,16 +74,15 @@ 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 - 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 +93,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 +118,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 +137,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,32 +152,119 @@ 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 winning_move(self, piece): + 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). + """ + 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): + """ + 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 _set_win_square(self, piece, r, c, direction: str): + """ + 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. + :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) + else: + check = self.horizontal_win(piece, r, c) + + 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 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 + :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. :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 + + 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): @@ -146,11 +272,10 @@ def tie_move(self): Checks for a tie game. :return: Whether a tie has occurred. """ - slots_filled: int = 0 + return self.slots_filled == (self.rows * self.cols) - for c in range(self.cols): - for r in range(self.rows): - if self.board[r][c] != 0: - slots_filled += 1 + def __str__(self): + return str(flip(self.board, 0)) - return slots_filled == 42 + def __iter__(self): + return iter(self.board) diff --git a/game_data.py b/game_data.py index a7ae2fc..a4cafa4 100644 --- a/game_data.py +++ b/game_data.py @@ -13,14 +13,17 @@ class GameData: width: int sq_size: int size: Tuple[int, int] + game_over: bool turn: int 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 = []