# Principled Programming / Tim Teitelbaum / Chapter 15.
from mrp import MRP
import random
class RunMaze:
    """
    Rat running.

    # Methods.
    main(cls) -> None

    # See also.
    Chapter 15 of Principled Programming
    """

    @classmethod
    def main(cls) -> None:
            """Run a maze given as input, if possible."""
        # Input a maze of arbitrary size, or output “malformed input”
        #   and stop if the input is improper. Input format: TBD.
            RunMaze._input()
        # Compute a direct path through the maze, if one exists.
            RunMaze._solve()
        # Output the direct path found, or “unreachable” if there is
        #    none. Output format: TBD.
            RunMaze._output()
        # Stop execution if path found is not a solution.
            assert MRP.is_solution(), "internal program error"

    @classmethod
    def _input(cls) -> None:
        """Input a maze of arbitrary size, or output “malformed input”  and stop if the input is improper."""
        MRP.input()

    @classmethod
    def _solve(cls) -> None:
        """Compute a direct path through the maze, if one exists."""
        while not(MRP.is_at_cheese()) and not(MRP.is_about_to_repeat()):
            if MRP.is_facing_wall(): MRP.turn_clockwise()
            elif MRP.is_facing_unvisited():
                MRP.step_forward()
                MRP.turn_counter_clockwise() 
            else: RunMaze._retract()

    @classmethod
    def _retract(cls) -> None:
        """Unwind abortive exploration."""
        MRP.record_neighbor_and_direction()
        while not(MRP.is_at_neighbor()):
            MRP.face_previous()
            MRP.step_backward()
        MRP.restore_direction()
        MRP.turn_counter_clockwise()

    @classmethod
    def _output(cls) -> None:
        """Output the direct path found, or “unreachable” if there is none."""
        if not(MRP.is_at_cheese()): print ("Unreachable");
        else: MRP.print_maze()

    @classmethod
    def exhaustive_test(cls) -> None:
        """Generate/solve all mazes of sizes up through 3, and validate paths found."""
        for N in range(1, 5):
            for w in range(0, 2 ** (2 * N *(N - 1))):
                MRP.generate_input(N, w)
                RunMaze._solve()
                assert MRP.is_solution(), "internal program error"
                if (w > 0) and (w%100000 == 0): print(w) # Heartbeat.
            print("passed for size:", N)
        print("passed")

    @classmethod
    def fuzz_test(cls, f: int, n: int) -> None:
        """Fuzz F mazes of size N."""
        for k in range(0, f): RunMaze.random_test(n)

    @classmethod
    def random_test(cls, n: int) -> None:
            """Create a random maze of size N."""
        # Let w be random walls for the n-by-n maze.
            w = random.randint(0, 2 ** (2 * n * (n - 1)))
        # Generate maze of size n with walls w, solve, and validate (or abort).
            print("Size:", n, "Walls:", w) # (Comment out for serious test.)
            MRP.generate_input(n, w)        # Create n-by-n maze with walls w.
            RunMaze._solve()               # Attempt a solution.
            assert MRP.is_solution()       # Validate solution (or abort).
            MRP.print_maze()               # (Comment out for serious test.)

        
# Uncomment to run on builtin example.
RunMaze.main()
# Uncomment to run on all mazes of size <= 4.
#RunMaze.exhaustive_test()
# Uncomment to run on 25 random mazes of size 5.
#RunMaze.fuzz_test(25, 5)