Example #1
0
    def _get_shortest_path(self, src_id, dst_id, *, first_axis=0):
        """Return the MoveComp with the smallest distance that will take src_id to dst_id. Non-public method.

        Args:
            src_id
            dst_id
            first_axis: (optional) The first axis the src_id should move along. By default, 0.

        Returns:
            movecomp: The MoveComp representing that shortest path.
        """
        src_multi_index = self._get_multi_index_from_id(src_id)
        dst_multi_index = self._get_multi_index_from_id(dst_id)
        multi_shift = tuple(
            _smallest_shift(dst_axis_index - src_axis_index, axis_len)
            for src_axis_index, dst_axis_index, axis_len in zip(
                src_multi_index, dst_multi_index, self.shape))

        if first_axis == 0:
            axes = (0, 1)
        elif first_axis == 1:
            axes = (1, 0)
        else:
            raise IndexError("first_axis must be 1 or 0. ")

        movecomp = MoveComp()
        for i, axis in enumerate(axes):
            shift = multi_shift[axis]
            index_ = [src_multi_index[axis ^ 1], dst_multi_index[axis ^ 1]][i]
            movecomp.append(Move(axis, index_, shift))

        return movecomp
Example #2
0
    def apply_action_strs(self, action_strs):
        """Apply the action described in these strs.

        Args:
            action_strs: Sequence of Moves encoded through move_str grammar.
        """
        self.apply_action(MoveComp.from_strs(action_strs))
Example #3
0
    def get_random_movecomp(self, len_=1):
        """Return a random MoveComp.

        Args:
            len_: (optional) The number of Moves in the returned MoveComp. By default, 1.

        Returns:
            movecomp: a random MoveComp.
        """
        return MoveComp([self._get_random_move() for _ in range(len_)])
Example #4
0
    def move(self, movecomp):
        """Apply the transformation encoded in movecomp.

        Args:
            movecomp: MoveComp or alike object describing the transformation to apply.
        """
        movecomp = MoveComp(movecomp)

        for move in movecomp:
            for board in (self._board, self._ids):
                if move.axis == 0:
                    board[:, move.index_] = np.roll(board[:, move.index_],
                                                    move.shift)
                else:
                    board[move.index_, :] = np.roll(board[move.index_, :],
                                                    move.shift)

            self.applied_moves.append(move)
Example #5
0
 def test_as_strs(self):
     from movecomp import MoveComp
     from loopover_puzzle import LoopoverPuzzle
     loopover_puzzle = LoopoverPuzzle.from_shape((10, 10), randomize=True)
     movecomp = loopover_puzzle.get_random_movecomp(10)
     assert MoveComp.from_strs(movecomp.as_strs) == movecomp
Example #6
0
 def __init__(self, board, *, ids=None):
     super().__init__(board, ids=ids)
     self.applied_moves = MoveComp()
Example #7
0
    def _get_setup_movecomp(self, rot):
        """Return the MoveComp needed to setup the board in order to be able to apply the operation to perform the rot.

        Non-public method.

        Args:
            rot: Rot to setup for. Needs to be a tri.

        Returns:
            setup_movecomp: MoveComp needed for setup.
            main_id: Id of the main piece in the rot.
            id_a: Id of piece A.
            id_b: Id of piece B.
        """
        setup_movecomp = MoveComp()

        center_id = self._get_center_id(rot)

        # Finding both possible paths from each id of rot to the center id.
        paths_to_center = []
        for id_ in rot:
            paths_per_id = (
                self._get_shortest_path(id_, center_id, first_axis=0),
                self._get_shortest_path(id_, center_id, first_axis=1),
                id_,
            )
            paths_to_center.append(paths_per_id)

        # In order of total distance, finding one id that's already aligned on at least one axis with center_id, to
        # move it there for the beginning of the setup movecomp:
        paths_to_center.sort(key=lambda x: x[0].distance)
        for path_to_center in paths_to_center:
            if path_to_center[0][0] == 0 or path_to_center[0][1] == 0:
                paths_to_center.remove(path_to_center)
                main_id = path_to_center[2]
                path_main_id_to_center = path_to_center[0]
                break
        else:
            raise
        setup_movecomp += path_main_id_to_center

        # Unpacking paths_to_center for readability:
        paths_id_a, paths_id_b = paths_to_center
        path_id_a_first_axis_0, path_id_a_first_axis_1, id_a = paths_id_a
        path_id_b_first_axis_0, path_id_b_first_axis_1, id_b = paths_id_b

        if path_id_a_first_axis_0[0] == 0:
            if path_id_b_first_axis_0[0] == 0:
                move_a_axis, move_a_index, _ = path_id_a_first_axis_0[0]
                move_a = Move(move_a_axis, move_a_index, 1)
                move_b_axis, move_b_index, move_b_shift = path_id_a_first_axis_0[
                    1]
                move_b = Move(move_b_axis,
                              (move_b_index + 1) % self.shape[move_b_axis ^ 1],
                              move_b_shift)
                setup_movecomp += MoveComp([move_a, move_b])
            else:
                setup_movecomp += MoveComp(
                    [path_id_a_first_axis_0[0], path_id_b_first_axis_1[0]])
            return setup_movecomp, main_id, id_a, id_b

        if path_id_a_first_axis_1[0] == 0:
            if path_id_b_first_axis_1[0] == 0:
                move_a_axis, move_a_index, _ = path_id_a_first_axis_1[0]
                move_a = Move(move_a_axis, move_a_index, 1)
                move_b_axis, move_b_index, move_b_shift = path_id_a_first_axis_1[
                    1]
                move_b = Move(move_b_axis,
                              (move_b_index + 1) % self.shape[move_b_axis ^ 1],
                              move_b_shift)
                setup_movecomp += MoveComp([move_a, move_b])
            else:
                setup_movecomp += MoveComp(
                    [path_id_a_first_axis_1[0], path_id_b_first_axis_0[0]])
            return setup_movecomp, main_id, id_a, id_b

        if path_id_b_first_axis_1[0] == 0:
            setup_movecomp += MoveComp(
                [path_id_a_first_axis_0[0], path_id_b_first_axis_1[0]])
        else:
            setup_movecomp += MoveComp(
                [path_id_a_first_axis_1[0], path_id_b_first_axis_0[0]])

        return setup_movecomp, main_id, id_a, id_b
Example #8
0
class LoopoverPuzzle(Puzzle):
    """Represents a puzzle game with pieces arranged in a grid. These pieces can slide horizontally and vertically,
    where in order to let a row or column slide, a piece will jump over to the other side of the row or column,
    in what is called a Move.
    """
    def __init__(self, board, *, ids=None):
        super().__init__(board, ids=ids)
        self.applied_moves = MoveComp()

    @classmethod
    def from_shape(cls, shape, *, randomize=False):
        if len(shape) != 2:
            raise PuzzleDimError
        return super().from_shape(shape, randomize=randomize)

    def get_solution_as_strs(self):
        """Return the solution to self as strs using move_str grammar. """
        solution = self.get_solution()
        if solution is None:
            return None

        return solution.as_strs

    def get_solution(self):
        working_perm = self.copy()

        while True:
            rotcomp = working_perm.get_rotcomp_solution()
            try:
                rotcomp.to_tris(be_strict=True)
            except RotCompSubdivideError:
                for axis, dim in enumerate(working_perm.shape):
                    if dim % 2 == 0:
                        working_perm.move(Move(axis, 0, 1))
                        break

                else:
                    return None

            else:
                break

        working_perm.rot(rotcomp)

        solution = working_perm.applied_moves.compressed()

        return solution

    def apply_action_strs(self, action_strs):
        """Apply the action described in these strs.

        Args:
            action_strs: Sequence of Moves encoded through move_str grammar.
        """
        self.apply_action(MoveComp.from_strs(action_strs))

    def apply_action(self, action):
        """Apply the given action to self.

        Args:
            action: MoveComp or alike to apply to self.
        """
        self.move(action)

    def randomize_perm(self):
        movecomp = self.get_random_movecomp(len_=self.n_pieces, )

        self.move(movecomp)

    def rot(self, rotcomp):
        """Apply a RotComp or alike transformation to self.

        Args:
            rotcomp: A RotComp or RotComp like sequence of tris (ie Rots of len 3), exclusively.

        Raises:
            LoopoverPuzzleRotError: if one of the Rots in rotcomp is not a tri.
        """
        rotcomp = RotComp(rotcomp)

        if rotcomp.count_by_len(3) != len(rotcomp):
            raise LoopoverPuzzleRotError

        for rot in rotcomp:
            # Setup:
            setup_movecomp, main_id, id_a, id_b = self._get_setup_movecomp(rot)
            self.move(setup_movecomp)

            # Operation:
            op_a = self._get_shortest_path(id_a, main_id)
            op_b = self._get_shortest_path(id_b, main_id)
            rot.roll_to(main_id, to_front=False)
            if rot[1] == id_b:
                op_movecomp = op_a + op_b - op_a - op_b
            else:
                op_movecomp = op_b + op_a - op_b - op_a
            self.move(op_movecomp)

            # Reversing setup:
            self.move(-setup_movecomp)

    def move(self, movecomp):
        """Apply the transformation encoded in movecomp.

        Args:
            movecomp: MoveComp or alike object describing the transformation to apply.
        """
        movecomp = MoveComp(movecomp)

        for move in movecomp:
            for board in (self._board, self._ids):
                if move.axis == 0:
                    board[:, move.index_] = np.roll(board[:, move.index_],
                                                    move.shift)
                else:
                    board[move.index_, :] = np.roll(board[move.index_, :],
                                                    move.shift)

            self.applied_moves.append(move)

    def get_random_movecomp(self, len_=1):
        """Return a random MoveComp.

        Args:
            len_: (optional) The number of Moves in the returned MoveComp. By default, 1.

        Returns:
            movecomp: a random MoveComp.
        """
        return MoveComp([self._get_random_move() for _ in range(len_)])

    def _get_setup_movecomp(self, rot):
        """Return the MoveComp needed to setup the board in order to be able to apply the operation to perform the rot.

        Non-public method.

        Args:
            rot: Rot to setup for. Needs to be a tri.

        Returns:
            setup_movecomp: MoveComp needed for setup.
            main_id: Id of the main piece in the rot.
            id_a: Id of piece A.
            id_b: Id of piece B.
        """
        setup_movecomp = MoveComp()

        center_id = self._get_center_id(rot)

        # Finding both possible paths from each id of rot to the center id.
        paths_to_center = []
        for id_ in rot:
            paths_per_id = (
                self._get_shortest_path(id_, center_id, first_axis=0),
                self._get_shortest_path(id_, center_id, first_axis=1),
                id_,
            )
            paths_to_center.append(paths_per_id)

        # In order of total distance, finding one id that's already aligned on at least one axis with center_id, to
        # move it there for the beginning of the setup movecomp:
        paths_to_center.sort(key=lambda x: x[0].distance)
        for path_to_center in paths_to_center:
            if path_to_center[0][0] == 0 or path_to_center[0][1] == 0:
                paths_to_center.remove(path_to_center)
                main_id = path_to_center[2]
                path_main_id_to_center = path_to_center[0]
                break
        else:
            raise
        setup_movecomp += path_main_id_to_center

        # Unpacking paths_to_center for readability:
        paths_id_a, paths_id_b = paths_to_center
        path_id_a_first_axis_0, path_id_a_first_axis_1, id_a = paths_id_a
        path_id_b_first_axis_0, path_id_b_first_axis_1, id_b = paths_id_b

        if path_id_a_first_axis_0[0] == 0:
            if path_id_b_first_axis_0[0] == 0:
                move_a_axis, move_a_index, _ = path_id_a_first_axis_0[0]
                move_a = Move(move_a_axis, move_a_index, 1)
                move_b_axis, move_b_index, move_b_shift = path_id_a_first_axis_0[
                    1]
                move_b = Move(move_b_axis,
                              (move_b_index + 1) % self.shape[move_b_axis ^ 1],
                              move_b_shift)
                setup_movecomp += MoveComp([move_a, move_b])
            else:
                setup_movecomp += MoveComp(
                    [path_id_a_first_axis_0[0], path_id_b_first_axis_1[0]])
            return setup_movecomp, main_id, id_a, id_b

        if path_id_a_first_axis_1[0] == 0:
            if path_id_b_first_axis_1[0] == 0:
                move_a_axis, move_a_index, _ = path_id_a_first_axis_1[0]
                move_a = Move(move_a_axis, move_a_index, 1)
                move_b_axis, move_b_index, move_b_shift = path_id_a_first_axis_1[
                    1]
                move_b = Move(move_b_axis,
                              (move_b_index + 1) % self.shape[move_b_axis ^ 1],
                              move_b_shift)
                setup_movecomp += MoveComp([move_a, move_b])
            else:
                setup_movecomp += MoveComp(
                    [path_id_a_first_axis_1[0], path_id_b_first_axis_0[0]])
            return setup_movecomp, main_id, id_a, id_b

        if path_id_b_first_axis_1[0] == 0:
            setup_movecomp += MoveComp(
                [path_id_a_first_axis_0[0], path_id_b_first_axis_1[0]])
        else:
            setup_movecomp += MoveComp(
                [path_id_a_first_axis_1[0], path_id_b_first_axis_0[0]])

        return setup_movecomp, main_id, id_a, id_b

    def _get_shortest_path(self, src_id, dst_id, *, first_axis=0):
        """Return the MoveComp with the smallest distance that will take src_id to dst_id. Non-public method.

        Args:
            src_id
            dst_id
            first_axis: (optional) The first axis the src_id should move along. By default, 0.

        Returns:
            movecomp: The MoveComp representing that shortest path.
        """
        src_multi_index = self._get_multi_index_from_id(src_id)
        dst_multi_index = self._get_multi_index_from_id(dst_id)
        multi_shift = tuple(
            _smallest_shift(dst_axis_index - src_axis_index, axis_len)
            for src_axis_index, dst_axis_index, axis_len in zip(
                src_multi_index, dst_multi_index, self.shape))

        if first_axis == 0:
            axes = (0, 1)
        elif first_axis == 1:
            axes = (1, 0)
        else:
            raise IndexError("first_axis must be 1 or 0. ")

        movecomp = MoveComp()
        for i, axis in enumerate(axes):
            shift = multi_shift[axis]
            index_ = [src_multi_index[axis ^ 1], dst_multi_index[axis ^ 1]][i]
            movecomp.append(Move(axis, index_, shift))

        return movecomp

    def _get_random_move(self):
        """Return a random Move. Non-public method.

        Returns:
            move: a random Move.
        """
        axis = random.randint(0, 1)
        index_ = random.randint(0, self.shape[axis ^ 1] - 1)
        shift = random.randint(1, self.shape[axis] - 1)

        return Move(axis, index_, shift)

    def _get_center_id(self, ids):
        """Return an id such that the cumulative distance from it to the given ids is minimized. Non-public method.

        Args:
            ids: The ids to find the center for.

        Returns:
            center_id: The center for these ids.
        """
        multi_indices = zip(*(self._get_multi_index_from_id(id_)
                              for id_ in ids))

        mean_multi_index = []
        for axis_len, axis_indices in zip(self.shape, multi_indices):
            mean_multi_index.append(_modular_median(axis_indices, axis_len))

        return self._ids[tuple(mean_multi_index)]

    def _get_shifted_id(self, id_, multi_shift):
        """Return the id located at id_ + multi_shift. Non-public method.

        Args:
            id_
            multi_shift

        Returns:
            shifted_id: Id located at id_ + multi_shift.
        """
        multi_index = self._get_multi_index_from_id(id_)
        shifted_multi_index = tuple(
            (axis_index + axis_shift) % axis_len
            for axis_index, axis_shift, axis_len in zip(
                multi_index, multi_shift, self.shape))

        return self._ids[shifted_multi_index]

    def _get_pretty_repr(self, *, use_ids=False):
        board_to_repr = self._board if not use_ids else self._ids
        pretty_repr = tabulate(board_to_repr, tablefmt="fancy_grid")
        return pretty_repr