Example #1
0
    def search_for_one_move(self, search_grid: Grid, move_index: int) -> int:
        """
        Performs search for a given number. 'searches_per_moves' iterations are performed
        and then all the acquired scores are summed.

        Parameters
        ----------
        search_grid: Grid
            Grid for which current search will be performed.
        move_index: int
            Index in a list of a first move in a search.

        Returns
        -------
        int
            Sum of scores in all simulated games.
        """
        search_score: int = 0
        search_moves: List[Callable[[], bool]] = get_moves_list(search_grid)

        is_valid: bool = search_moves[move_index]()

        if not is_valid:
            return 0

        search_grid.generate_twos(number_of_twos=1)
        current_grid: np.ndarray = np.copy(search_grid.grid)
        current_score: int = search_grid.score
        for _ in range(self.searches_per_move):
            search_grid.grid = np.copy(current_grid)
            search_grid.score = current_score
            for _ in range(self.moves_per_search):
                if search_grid.is_win() or search_grid.is_lose():
                    break
                is_valid = make_random_move(search_moves)
                if is_valid:
                    search_grid.generate_twos(number_of_twos=1)
            search_score += search_grid.score

        return search_score
Example #2
0
class Game:
    """
    A class implementing main menu and graphics for 2048.
    Attributes
    ---------
    config: dict
        A JSON which contains configuration of the game (colors, sizes, coordinates etc.).
    best_score: int
        Best saved score obtained in 2048.
    grid_size: int
        Size of the game's grid.
    grid: Grid
        Class which implements logic of 2048.
    screen: pygame.surface.Surface:
        Instance of pygame's screen.
    fonts: Dict[str, pygame.font.Font]
        Fonts used by texts displayed during game_loop or
        while in main menu.
    buttons: Dict[str, Button]
        Dict of all required buttons for both game and
        main menu.
    """
    def __init__(self, config: dict) -> None:
        """
        Parameters
        ----------
        config: dict
            A JSON which contains configuration of the game (colors, sizes, coordinates etc.).
        """
        self.config: dict = config
        self.best_score: int = self.config["best_score"]
        self.grid_size: int = self.config['grid_size']
        self.grid: Grid = Grid()

        pygame.init()

        # pygame's attributes
        self.screen: pygame.surface.Surface = \
            pygame.display.set_mode((config['size'],
                                     config['size'] + self.config["header_height"]))

        self.fonts: Dict[str, pygame.font.Font] = {
            "text_font":
            pygame.font.SysFont(self.config['font'],
                                self.config['text_font_size'],
                                bold=True),
            "button_font":
            pygame.font.SysFont(self.config['font'],
                                self.config['button_font_size'],
                                bold=True),
            "title_font":
            pygame.font.SysFont(self.config['font'],
                                self.config['title_font_size'],
                                bold=True)
        }
        self.buttons: Dict[str, Button] = {
            "menu":
            Button(
                self.fonts["title_font"],
                ButtonColors(self.config["color"]["2048"],
                             self.config["color"]["2048"],
                             self.config["color"]["white"]),
                (10, 15, 150, 120)),
            "play":
            Button(
                self.fonts["button_font"],
                ButtonColors(self.config["color"]["play"],
                             self.config["color"]["64"],
                             self.config["color"]["black"]),
                (105, 400, 300, 45)),
            "monte_carlo":
            Button(
                self.fonts["button_font"],
                ButtonColors(self.config["color"]["play"],
                             self.config["color"]["64"],
                             self.config["color"]["black"]),
                (105, 500, 300, 45)),
            "reset":
            Button(
                self.fonts["button_font"],
                ButtonColors(self.config["color"]["play"],
                             self.config["color"]["64"],
                             self.config["color"]["black"]),
                (105, 250, 300, 45)),
            "current_score":
            Button(
                self.fonts["button_font"],
                ButtonColors(self.config['color']['score'],
                             self.config["color"]["64"],
                             self.config["color"]["white"]),
                (190, 15, 150, 60)),
            "best":
            Button(
                self.fonts["button_font"],
                ButtonColors(self.config['color']['score'],
                             self.config["color"]["64"],
                             self.config["color"]["white"]),
                (345, 15, 150, 60))
        }

        # initialize pygame
        pygame.display.set_caption('2048')
        icon: pygame.surface.Surface = pygame.transform.scale(
            pygame.image.load("images/game_icon.ico"), (32, 32))
        pygame.display.set_icon(icon)

    def check_game_status(self) -> None:
        """
        Check if game is won/lost. If it is true message is
        displayed and return to menu via button is enabled.
        """
        if self.grid.is_win() or self.grid.is_lose():
            size: int = self.config['size']
            screen = pygame.Surface(
                (size, size + self.config["header_height"]), pygame.SRCALPHA)
            screen.fill(self.config['color']['over'])
            self.screen.blit(screen, (0, 0))

            if self.grid.is_win():
                info: str = 'YOU WIN!'
                coords: Tuple[int, int] = (160, 180)
            else:
                info = 'GAME OVER!'
                coords = (140, 180)

            self.screen.blit(
                self.fonts["text_font"].render(info, True,
                                               self.config["color"]["dark"]),
                coords)
            while True:
                self.buttons["reset"].draw(self.screen, "Menu")
                pygame.display.update()

                for event in pygame.event.get():
                    pos: Tuple[int, int] = pygame.mouse.get_pos()
                    if self.buttons["reset"].handle_event(event, pos):
                        self.show_menu()

    def start_game(self) -> None:
        """
        Initialize Grid object and starts game loop.
        """
        self.grid = Grid()
        self.display()
        self.screen.blit(
            self.fonts["text_font"].render("NEW GAME!", True,
                                           self.config['color']['dark']),
            (140, 375))
        pygame.display.update()

        time.sleep(1)
        self.grid.generate_twos(number_of_twos=2)
        self.display()

    def display(self) -> None:
        """
        Generates objects during game loop.
        Created objects:
            - Score's banners.
            - Menu's button.
            - Game's grid.
        """
        self.screen.fill(self.config['color']['background'])
        box: int = self.config['size'] // 4
        padding = self.config['padding']

        self.buttons["menu"].draw(self.screen, "2048")
        self.buttons["current_score"].draw(self.screen,
                                           f"SCORE: {self.grid.score}")
        self.buttons["best"].draw(self.screen, f"BEST: {self.best_score}")

        for i in range(self.grid_size):
            for j in range(self.grid_size):
                color: Tuple[int, int, int] =\
                    self.config['color'][str(self.grid[i, j])]
                pygame.draw.rect(self.screen, color,
                                 (j * box + padding, i * box + padding +
                                  self.config["header_height"],
                                  box - 2 * padding, box - 2 * padding), 0)
                if self.grid[i, j] != 0:
                    if self.grid[i, j] in (2, 4):
                        text_color = self.config['color']['dark']
                    else:
                        text_color = self.config['color']['light']

                    self.screen.blit(
                        self.fonts["text_font"].render(f"{self.grid[i, j]:>4}",
                                                       True, text_color),
                        (j * box + 4 * padding,
                         i * box + 7 * padding + self.config["header_height"]))

            pygame.display.update()

    def update_grid(self, current_grid: np.ndarray) -> None:
        """
        If after move grid changed 2/4 is added to a grid, then screen is updated.
        At the end checks for ether win or lose.
        """
        if not np.array_equal(self.grid, current_grid):
            self.grid.generate_twos(number_of_twos=1)
            self.display()
            self.check_game_status()

    def bot_move(self, bot: MonteCarloTreeSearch,
                 all_moves: List[Callable[[], bool]]) -> None:
        """
        Performs a move returned by bot, then checks if grid was updated.

        Parameters
        ----------
        bot: MonteCarloTreeSearch
            Bot object which performs Monte Carlo Search to find best next move.
        all_moves: List[Callable[[], bool]]
            List containing all possible grid's move.
        """
        current_grid: np.ndarray = np.copy(self.grid.grid)
        all_moves[bot(asynchronous=True)]()
        self.update_grid(current_grid)

    def player_move(self, event: pygame.event.Event) -> None:
        """
        Performs move given by a player, then checks if grid was updated.

        Parameters
        ----------
        event: pygame.event.Event
            Event in which method checks if player performed grid's move.
        """
        if str(event.key) in self.config['keys']:
            key = self.config['keys'][str(event.key)]

            current_grid = np.copy(self.grid.grid)
            self.grid.move(key)
            self.best_score = max(self.best_score, self.grid.score)
            self.update_grid(current_grid)

    def game_loop(self, use_bot: bool) -> None:
        """
        Game loop in which either player or bot make
        moves until games is finished. After game is over by clicking
        menu button user can go back to main menu. Loop can be broken
        either by pressing q, exiting created game's window or pressing
        return button.

        Parameters
        ----------
        use_bot: bool
            Boolean which determines if player or bot is playing.
        """
        self.start_game()
        bot: MonteCarloTreeSearch = MonteCarloTreeSearch(self.grid)
        all_moves_list: List[Callable[[], bool]] = get_moves_list(self.grid)

        while True:
            if use_bot:
                self.bot_move(bot, all_moves_list)

            for event in pygame.event.get():
                pos: Tuple[int, int] = pygame.mouse.get_pos()
                if Game.check_for_quit(event):
                    break

                if not use_bot and event.type == pygame.KEYDOWN:
                    self.player_move(event)

                if self.buttons["menu"].handle_event(event, pos):
                    self.show_menu()

    def show_menu(self) -> None:
        """
        Generates main menu.
        Player or bot path can be chosen.
        """
        while True:
            self.screen.fill(self.config["color"]["background"])

            self.screen.blit(
                pygame.transform.scale(
                    pygame.image.load("images/game_icon.ico"), (300, 300)),
                (100, 50))

            self.buttons["play"].draw(self.screen, "Play")
            self.buttons["monte_carlo"].draw(self.screen, "Monte Carlo")
            pygame.display.update()

            for event in pygame.event.get():
                pos: Tuple[int, int] = pygame.mouse.get_pos()
                if Game.check_for_quit(event):
                    break

                if self.buttons["play"].handle_event(event, pos):
                    self.game_loop(use_bot=False)
                if self.buttons["monte_carlo"].handle_event(event, pos):
                    self.game_loop(use_bot=True)

    @staticmethod
    def check_for_quit(event: pygame.event.Event) -> bool:
        """
        Checks if game's window was closed or 'q' was pressed if yes game is shut down.

        Parameters
        ----------
        event: pygame.event.Event
            Pygame's event in which method checks if game should shut down.

        Returns
        -------
        bool
            True if player chose to quit.
        """
        if event.type == pygame.QUIT or event.type == pygame.KEYDOWN and event.key == pygame.K_q:
            pygame.quit()
            return True

        return False
class MCTSTestCase(unittest.TestCase):
    """
    Case which tests if Monte Carlo Tree Search
    bot is working correctly.
    """
    def setUp(self) -> None:
        """
        Initialize required objects for all the tests.
        """
        self.grid: Grid = Grid()
        self.bot: MonteCarloTreeSearch = MonteCarloTreeSearch(self.grid)

    def test_move_list(self) -> None:
        """
        Tests if list of all the moves is created properly.
        """
        list_of_moves: List[Callable[[], bool]] = get_moves_list(self.grid)
        self.assertEqual(len(list_of_moves), self.grid.grid_size)

        self.grid.grid = np.array([[0, 2, 0, 0], [0, 0, 0, 0], [0, 0, 4, 2],
                                   [2, 0, 0, 4]])
        list_of_moves[0]()
        self.assertTrue((self.grid.grid == [[2, 2, 4, 2], [0, 0, 0, 4],
                                            [0, 0, 0, 0], [0, 0, 0, 0]]).all())
        list_of_moves[1]()
        self.assertTrue((self.grid.grid == [[0, 0, 0, 0], [0, 0, 0, 0],
                                            [0, 0, 0, 2], [2, 2, 4, 4]]).all())
        list_of_moves[2]()
        self.assertTrue((self.grid.grid == [[0, 0, 0, 0], [0, 0, 0, 0],
                                            [2, 0, 0, 0], [4, 8, 0, 0]]).all())
        list_of_moves[3]()
        self.assertTrue((self.grid.grid == [[0, 0, 0, 0], [0, 0, 0, 0],
                                            [0, 0, 0, 2], [0, 0, 4, 8]]).all())

    def test_random_move(self) -> None:
        """
        Tests if move was performed after call
        of random_move function.
        """
        test_grid: np.ndarray = np.array([[0, 0, 0, 0], [0, 2, 2, 0],
                                          [0, 2, 2, 0], [0, 0, 0, 0]])
        self.grid.grid = np.copy(test_grid)
        make_random_move(get_moves_list(self.grid))

        self.assertFalse((test_grid == self.grid.grid).all())

    @unittest.skipIf(False, "Set to False if bot verification not needed.")
    def test_bot(self) -> None:
        """
        Tests if bot works as designed.
        """
        self.grid = Grid()
        self.bot.grid = self.grid
        search_move_list: List[Callable[[], bool]] = get_moves_list(self.grid)

        self.grid.generate_twos(number_of_twos=2)
        while not (self.grid.is_win() or self.grid.is_lose()):
            ind = self.bot()
            search_move_list[ind]()
            self.grid.generate_twos(number_of_twos=1)
            print(self.grid)

        self.assertTrue(self.grid.is_win())
        self.assertFalse(self.grid.is_lose())