Can you rewrite this code so it does not need __hash__?

import heapq

class Board:
    def __init__(self, dimension, blocks):
        self._dimension = dimension
        self._blocks = blocks

    def dimension(self):
        return self._dimension

    def hamming(self):
        count = 0
        N = self._dimension
        goal_value = 1
        for i in range(N):
            for j in range(N):
                value = self._blocks[i][j]
                if value != 0 and value != goal_value:
                    count += 1
                goal_value += 1
        return count

    def manhattan(self):
        distance = 0
        N = self._dimension
        for i in range(N):
            for j in range(N):
                value = self._blocks[i][j]
                if value != 0:
                    goal_row = (value - 1) // N
                    goal_col = (value - 1) % N
                    dist = abs(i - goal_row) + abs(j - goal_col)
                    distance += dist
        return distance

    def is_goal(self):
        N = self._dimension
        goal_value = 1
        for i in range(N):
            for j in range(N):
                if i == N - 1 and j == N - 1:
                    if self._blocks[i][j] != 0:
                        return False
                else:
                    if self._blocks[i][j] != goal_value:
                        return False
                    goal_value += 1
        return True

    def __eq__(self, other):
        return self._blocks == other._blocks

    def __hash__(self):
        return hash(tuple(tuple(row) for row in self._blocks))

    def neighbors(self):
        N = self._dimension
        neighbors = []
        zero_row, zero_col = None, None
        for i in range(N):
            for j in range(N):
                if self._blocks[i][j] == 0:
                    zero_row, zero_col = i, j
                    break
            if zero_row is not None:
                break

        moves = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        for dr, dc in moves:
            new_row, new_col = zero_row + dr, zero_col + dc
            if 0 <= new_row < N and 0 <= new_col < N:
                new_blocks = [row[:] for row in self._blocks]
                new_blocks[zero_row][zero_col], new_blocks[new_row][new_col] = (
                    new_blocks[new_row][new_col],
                    new_blocks[zero_row][zero_col],
                )
                neighbor = Board(N, new_blocks)
                neighbors.append(neighbor)
        return neighbors

    def __str__(self):
        N = self._dimension
        lines = []
        for row in self._blocks:
            lines.append(' '.join(str(val).rjust(2) for val in row))
        return '\n'.join(lines)

class Solver:
    def __init__(self, board):
        self._initial_board = board
        self._solution = None
        self._moves = -1

    def is_solvable(self):
        N = self._initial_board.dimension()
        blocks = [val for row in self._initial_board._blocks for val in row if val != 0]
        inversions = 0
        for i in range(len(blocks)):
            for j in range(i + 1, len(blocks)):
                if blocks[i] > blocks[j]:
                    inversions += 1
        return inversions % 2 == 0

    def moves(self):
        if self._solution is None:
            self.solve()
        return self._moves

    def solution(self):
        if self._solution is None:
            self.solve()
        return self._solution

    def solve(self):
        if not self.is_solvable():
            self._moves = -1
            self._solution = None
            return

        class SearchNode:
            def __init__(self, board, moves, previous):
                self.board = board
                self.moves = moves
                self.previous = previous
                self.priority = self.moves + board.manhattan()

            def __lt__(self, other):
                return self.priority < other.priority

        open_set = []
        heapq.heappush(open_set, SearchNode(self._initial_board, 0, None))
        closed_set = set()

        while open_set:
            current_node = heapq.heappop(open_set)
            current_board = current_node.board

            if current_board.is_goal():
                self._moves = current_node.moves
                self._solution = []
                node = current_node
                while node is not None:
                    self._solution.append(node.board)
                    node = node.previous
                self._solution.reverse()
                return

            closed_set.add(current_board)

            for neighbor in current_board.neighbors():
                if neighbor in closed_set:
                    continue
                if current_node.previous and neighbor == current_node.previous.board:
                    continue
                neighbor_node = SearchNode(neighbor, current_node.moves + 1, current_node)
                heapq.heappush(open_set, neighbor_node)

        self._moves = -1
        self._solution = None

def read_board_data(filename):
    with open(filename, 'r') as f:
        lines = f.readlines()

    N = int(lines[0].strip())
    blocks = []

    for i in range(1, N + 1):
        row = list(map(int, lines[i].split()))
        blocks.append(row)

    return N, blocks

def main():
    board = Board(*read_board_data("input.txt"))
    solver = Solver(board)

    if not solver.is_solvable():
        print("Board does not have solutions")
    else:
        print(f"Minimal number of steps: {solver.moves()}")
        print("Solution path:")
        for step, board in enumerate(solver.solution()):
            print(f"Step {step}:\n{board}\n")

if __name__ == "__main__":
    main()