0

**Edit: I managed to find solution for my problem so i put the correct code in place. Everything works perfect now.

I'm trying to complete the last stage of a project in Jetbrains Academy but I can't make my approach to the problem work:

It gives the user the solution for the knights tour in chess if it exists by taking their inputs. I know that there are working solutions out there, but I would like to make it work with the approach I have taken.

I can't make it work. Although, it works fine when the player plays with and I had cut the extra lines.

Example:

Input: A 4x3 board, and 1 3 (column/row)

My implementation gives 2 possible moves [(3, 2), (2, 1)]. It then continues for the possible values of 2 1 and it goes all the way down until it has no other solutions. Then it stops. Those inputs works because they dont require further search in the inside possible valid move. It doesn't check multiple paths that exist at the deeper level.

I don't understand why this happens. Where is the problem in the code?

Initialization of the board (there are extra variables because of the previous steps):

# Write your code here
from copy import deepcopy
import sys
sys.setrecursionlimit(2000)
# With that recursion limit you can test boards up to 40x40 instantly.
# Without this you can test easily up to 30 x 30 any value
# and up to 35x35 any value except edge columns / rows ex 1 4 / 4 1.


class ChessBoard:

    def __init__(self, dimensions: str):
        self.game_over = False
        self.valid_grid = True
        self.dimensions = self.board_validation(dimensions.strip().split())
        if self.valid_grid:
            self.x = self.dimensions[0]
            self.y = self.dimensions[1]
            self.cell_length = len(str(self.x * self.y))  # Cell format helper for spaces
            self.board = [[(x, y) for x in range(1, self.x + 1)] for y in range(self.y, 0, -1)]
            self.visual = [["_" * self.cell_length for _ in range(1, self.x + 1)] for _ in range(self.y, 0, -1)]
            self.moves_visit = {key: False for i in range(len(self.board)) for key in self.board[i]}

            # We need duplicates to reverse the state of the board if we want:
            # to continue the game - try other initial move for the valid movements pool
            self.board_duplicate = deepcopy(self.board)
            self.visual_duplicate = deepcopy(self.visual)
            self.moves_visit_duplicate = deepcopy(self.moves_visit)

            self.x_boundaries = {i for i in range(1, self.x + 1)}
            self.y_boundaries = {i for i in range(1, self.y + 1)}
            self.valid_moves = []  # Helper for getting instant valid_moves for the player.
            self.is_first_move = True  # With that we make it easier to differentiate the first move from other inputs.
            self.movements_made = 0  # Move counter direct connection with the AI solution finder and the AI move
            self.board_dimensions = self.x * self.y
            self.has_solution = False
            self.initial_move_for_ai = None
            self.first_valid_move = set({})
            self.moves_placed = set({})  # Fast comparison to check if the ai_play function will return from recursion
            self.total_moves = 0
            self.find_solution_mode = False

    def set_first(self):
        if self.is_first_move:
            self.is_first_move = False

    def board_validation(self, board_dimensions):
        if len(board_dimensions) != 2:
            self.valid_grid = False
            return False
        try:
            x = int(board_dimensions[0])
            y = int(board_dimensions[1])
            if x <= 0 or y <= 0:
                self.valid_grid = False
                return False
            self.valid_grid = True
            return x, y
        except ValueError:
            self.valid_grid = False
            return False

    def move_validation(self, move_to_validate):
        if isinstance(move_to_validate, tuple) and move_to_validate[0] in self.x_boundaries and \
                move_to_validate[1] in self.y_boundaries:
            return move_to_validate
        values = move_to_validate.split()
        if len(values) != 2:
            return False
        try:
            column = int(values[0])
            row = int(values[1])
            movement = (column, row)
            if column not in self.x_boundaries or row not in self.y_boundaries:
                return False
            if not self.is_first_move and self.moves_visit[movement]:
                return False
            if not self.is_first_move and movement not in self.valid_moves:
                return False
            return movement
        except ValueError:
            return False

    # Knight movement. X for player - Movement number for AI
    def knight_move(self, movement, solution_mode=False):
        if self.movements_made == 0:
            self.set_first()
        movement = self.move_validation(movement)
        for m in range(len(self.board)):
            if movement in self.board[m]:
                index_of_move = self.board[m].index(movement)
                if not self.moves_visit[movement]:
                    if not solution_mode:
                        self.visual[m][index_of_move] = " " * (self.cell_length - 1) + "X"
                    else:
                        move = "" * (self.cell_length - 3) + str(self.movements_made + 1)
                        self.visual[m][index_of_move] = move.rjust(self.cell_length)
                    self.possible_moves(movement)
                    self.movements_made += 1
                    self.moves_visit[movement] = True
                return movement
        return False

    # Calculating exact moves to chose from. We return index based values based on visual repr
    # and also player based values (2, 1), (3, 2) etc.. columns / rows
    def move_calculation(self, movement_to_calc):
        movement = movement_to_calc
        value_to_process = [(-2, -1), (-2, 1), (-1, 2), (1, 2), (2, 1), (2, -1), (-1, -2), (1, -2)]
        possible_calculations = [(movement[0] + value_to_process[i][0], movement[1] +
                                  value_to_process[i][1]) for i in range(len(value_to_process))]

        final_possible_indexes = [(r, self.board[r].index(j)) for r in range(len(self.board))
                                  for j in possible_calculations if j in self.board[r] and not self.moves_visit[j]]

        final_possible_indexes_translation = [self.board[i][j] for i, j in final_possible_indexes]

        return final_possible_indexes, final_possible_indexes_translation

    # Function that is used to calculate how many moves we have for the valid placements on board
    def depth_calc(self, valid_move):
        x, y = self.move_calculation(valid_move)
        return len(x)

    # Similar to move_calculation but this one is used to register primary valid moves for player on board
    # And also to check the state of players game and outputting the depth values for the valid moves.
    def possible_moves(self, main_move):
        try:
            moves, moves_translation = self.move_calculation(main_move)
            combined_index_translation = set(zip(moves, moves_translation))
        except TypeError:
            return False

        if not self.find_solution_mode:
            for i, j in combined_index_translation:
                col, row = i[0], i[1]
                if not self.moves_visit[j]:
                    self.visual[col][row] = f'{" " * (self.cell_length - 1)}{self.depth_calc(j) - 1}'
            self.valid_moves = moves_translation
            print(self.__str__())
            self.board_updater()
            self.check_end()
        return moves_translation

    # Board update based on the moves_visit state True/False (if we visited or not)
    def board_updater(self):

        for i in range(len(self.board)):
            for j in range(len(self.board[i])):
                move_to_check = self.board[i][j]
                if self.moves_visit[move_to_check]:
                    self.visual[i][j] = f'{" " * (self.cell_length - 1)}*'
                else:
                    self.visual[i][j] = f'{" " * (self.cell_length - 1)}_'

    # Getting all primary valid moves to work with
    def get_valid_moves(self, from_move):
        valid = self.possible_moves(from_move)
        if valid:
            return valid
        else:
            return False

    # Key function for the ai_play function. It gives us the valid move with the minimum depth.
    def lower_move(self, valid_values):
        lengths = {i: self.depth_calc(i) for i in valid_values}
        min_depth = min(lengths, key=lengths.get)
        return min_depth

    # Recursive function that is giving us the solution for the inputted move
    def ai_play(self, move, pool, n):

        if not self.moves_placed:
            self.moves_placed.add(move)
        if n == 1:
            self.first_valid_move.add(self.lower_move(pool))
        if len(self.moves_placed) == self.board_dimensions:
            self.has_solution = True
            return True
        if self.total_moves >= self.board_dimensions ** 4:
            return False

        self.total_moves += 1

        if pool:
            best_move = self.lower_move(pool)
            ai_move = self.knight_move(best_move, solution_mode=True)
            self.moves_placed.add(ai_move)
            if ai_move:
                possible_moves = self.get_valid_moves(ai_move)
                if self.ai_play(ai_move, possible_moves, n + 1):
                    return True
        else:
            self.ai_play_reset()
            first_move = self.knight_reset()
            self.moves_placed.add(first_move)
            first_pool = self.get_valid_moves(first_move)
            self.first_valid_move.add(first_pool[0])
            new_pool = set(first_pool).difference(self.first_valid_move)
            try:
                self.ai_play(first_move, list(new_pool), 1)
            except RecursionError:
                self.ai_play_reset()
                return False
            except IndexError:
                print("We tested all initial valid moves and failed.\nChoose one cell higher column and try again")
                return False

    # Resets the important variables of the board. Crucial if ai_play doesn't find a solution with first try.
    def ai_play_reset(self):
        self.moves_placed.clear()
        self.board = deepcopy(self.board_duplicate)
        self.visual = deepcopy(self.visual_duplicate)
        self.moves_visit = deepcopy(self.moves_visit_duplicate)
        self.movements_made = 0
        self.has_solution = False
        self.first_valid_move.clear()

    # Checks the state of the player's game.
    def check_end(self):
        if not self.valid_moves:
            self.game_over = True
            if self.movements_made + 1 == self.board_dimensions:
                self.has_solution = True
                print("What a great tour! Congratulations!")
                return False
            else:
                print(f"No more possible moves!\nYour knight visited {self.movements_made + 1} squares!")
            return False

    # Resets the Knight position to the initial for the ai_play function
    def knight_reset(self):
        main_move = self.knight_move(self.initial_move_for_ai, solution_mode=True)
        return main_move

    # Helper function that manages the outputs of the ai_play function regarding if player want a solution or not
    def ai_solution_test(self, move, for_player=False):
        if self.board_dimensions in {16, 4, 9}:
            print("No solution exists!")
            return False
        self.initial_move_for_ai = move
        self.find_solution_mode = True
        movement = self.knight_move(move, solution_mode=True)
        self.set_first()
        self.initial_move_for_ai = movement
        valid_moves = self.get_valid_moves(movement)
        while True:
            self.ai_play(movement, valid_moves, 1)
            if self.has_solution:
                if not for_player:
                    print("\nHere's the solution!")
                    print(self.__str__())
                    break
                else:
                    break
        self.find_solution_mode = False
        self.ai_play_reset()
        return True

    def __str__(self):
        visualization = [" ".rjust(self.cell_length - 1) + "-" * (self.x * (self.cell_length + 1) + 3)]
        for i in range(len(self.visual)):
            main_chess = " ".join([str(len(self.visual) - i).rjust(self.cell_length - 1) + "|", *self.visual[i], "|"])
            visualization.append(main_chess)
        visualization.append(" " * (self.cell_length - 1) + "-" * (self.x * (self.cell_length + 1) + 3))
        visualization.append(
            " ".rjust(self.cell_length + 2) + " ".join([f"{i}".center(self.cell_length) for i in range(1, self.x + 1)]))
        return "\n".join(visualization)


def main():
    while True:
        board = ChessBoard(input("Enter your board's dimensions: "))
        if not board.valid_grid:
            print("Invalid dimensions!", end=" ")
            continue
        while True:
            knight_move = board.move_validation(input("Enter the knight's starting position: "))
            if not knight_move:
                print("Invalid move!", end=" ")
                continue
            else:
                break
        while True:
            puzzle_try = input("Do you want to try the puzzle? (y/n): ")
            if puzzle_try not in {"y", "n"}:
                print("Invalid input!", end=" ")
                continue
            else:
                break
        if puzzle_try == "y":
            check_if_solution = board.ai_solution_test(knight_move, for_player=True)
            if check_if_solution:
                board.knight_move(knight_move)
                while not board.game_over:
                    next_move = board.knight_move(input("Enter your next move: "))
                    if next_move:
                        board.board_updater()
                    else:
                        print("Invalid move!", end=" ")
                        continue
                    if board.game_over:
                        break
            else:
                break
        else:
            board.ai_solution_test(knight_move)
        break


if __name__ == "__main__":
    main()
Kostas
  • 13
  • 5
  • I looked 20 minutes at your code, but had to give up. It is too complex -- visualisation and core logic are mixed, class has too many properties, there are no comments, ... – trincot Aug 12 '22 at 08:27
  • I just refactored the whole code because there was too dependancy of each function to the state of the board and i was confused too. I placed explanatory comments and replaced the code. Can you check please? I think at the end it will be very fast to calculate the moves. The main problem is in the ai_play function , everything else is working exactly as they should. It checks for solutions as it should but is does it only for the first possible valid values. I cant think of a way to make it crawl the inside arrays that have more than one possible valid values. – Kostas Aug 12 '22 at 13:43
  • Rather than replacing the code in the question with something that works now, you should leave the question intact and put your working code in an answer. [You can answer your own question](https://stackoverflow.com/help/self-answer), and indeed, when you have a good solution, you're encouraged to do so. – Blckknght Aug 15 '22 at 09:29

0 Answers0