Beispiel #1
0
    def autolog(self, obj, report=False):
        puzzle = obj
        error_count = 0
        # For error reporting
        name = str(hash(puzzle)) if not puzzle.name else puzzle.name

        inconsistent_locations = puzzle.inconsistencies()
        if inconsistent_locations:
            self.log_error(puzzle, self.INCONSISTENT_BOARD)
            error_count += 1
            if report:
                # A blank board
                inconsistent_board = Board()
                for location in inconsistent_locations:
                    num = puzzle.get_cell(*location)
                    inconsistent_board.set_cell(num, *location)
                msg = 'puzzle has inconsistencies:\n\n'
                msg += frmt.strfboard(inconsistent_board, ascii_mode=True)
                msg += '\n\n'
                error.error(msg, prelude=name)

        # See the generator module for references on 17 being the minimum
        # for a proper Sudoku puzzle
        if puzzle.clue_count() < 17:
            self.log_error(puzzle, self.TOO_FEW_CLUES)
            error_count += 1
            if report:
                msg = 'puzzle contains fewer clues than the 17 required for'
                msg += ' a proper, single-solution Sudoku board\n'
                error.error(msg, prelude=name)

        return error_count
Beispiel #2
0
    def setUpClass(cls):
        super(TestControllerHelp, cls).setUpClass()
        cls.maxDiff = None

        # Name it to avoid changes to default name affecting test
        cls.puzzle = Board(name='test')
        cls.options = SolverController.Options()
        # So controller can quit without confirmation
        cls.options.assume_yes = True
Beispiel #3
0
    def setUpClass(cls):
        super(TestControllerExplain, cls).setUpClass()
        cls.maxDiff = None

        # Name it to avoid changes to default name affecting test
        cls.puzzle = Board(lines=cls.PUZZLE_LINES, name='test')
        cls.options = SolverController.Options()
        # So controller can quit without confirmation
        cls.options.assume_yes = True
        # To standardize output when `print` is called
        cls.options.width = 70
Beispiel #4
0
    def setUpClass(cls):
        assert len(cls.BREAKPOINTS
                   ) >= 3, 'Must have 4 or more breakpoints to test `delete`'
        super(TestControllerBreakpoint, cls).setUpClass()
        cls.maxDiff = None

        # Name it to avoid changes to default name affecting test
        cls.puzzle = Board(lines=cls.PUZZLE_LINES, name='test')
        cls.options = SolverController.Options()
        # So controller can quit without confirmation
        cls.options.assume_yes = True
        # To standardize output when `print` is called
        cls.options.width = 70
Beispiel #5
0
def get_puzzles_from_lines(lines, name=None, specify_lineno=False):
    """Return a list of puzzles formed from the given lines.

    Parameters
    ----------
    lines : list of iterable
        A list of 9 or more iterables (e.g., str, list, tuple), each of
        which is interpreted as a row in a puzzle if the iterable contains
        exactly 9 elements and no whitespace.
    name : str, optional
        The name to save in the Board instances (default 'stdin').
    specify_lineno : bool, optional
        True if the line number the puzzle begins at should be included in
        its name (e.g., 'puzzle.txt:1'), and False if not (default False).

    Returns
    -------
    list of Board instance
        A list of all puzzles found in the given lines.

    """
    if name is None:
        name = 'stdin'

    puzzles = []
    start_lineno = 0

    puzzle_lines = []
    for i, line in enumerate(lines):
        if len(line) != 9 or ' ' in line:
            continue

        if specify_lineno and start_lineno == 0:
            start_lineno = i + 1
        puzzle_lines.append(line)

        if len(puzzle_lines) == 9:
            final_name = name
            if specify_lineno:
                final_name += ':{}'.format(start_lineno)
                start_lineno = 0

            puzzle = Board(lines=puzzle_lines, name=final_name)
            if puzzle is not None:
                puzzles.append(puzzle)
            puzzle_lines = []

    return puzzles
Beispiel #6
0
def similar_puzzle(puzzle, seed, min_clues=17):
    """Return a puzzle similar to the given one using the provided seed.

    Return a satisfactory puzzle initialized with at least `min_clues`
    clues randomly selected from `puzzle` according to `seed`.

    Parameters
    ----------
    puzzle : Board instance
        The puzzle to base the new puzzle off of.
    seed : int, long, or hashable
        The seed to use for deriving the puzzle.
    min_clues : int, optional
        The minimum number of clues guaranteed to be in the derived puzzle
        (default 17).

    Returns
    -------
    Board instance
        The puzzle derived or None if `puzzle` had fewer clues than
        `min_clues`.

    See Also
    --------
    minimize : another method for generating a puzzle from a given puzzle
               that is (unlike this one) likely to generate a puzzle with
               the same solution as the original.

    Notes
    -----
    For an explanation of the default for `min_clues`, see the notes in
    `minimize`.

    """
    clues = puzzle.clues()
    if len(clues) < min_clues:
        return None

    random.seed(seed)
    random.shuffle(clues)

    new_puzzle = Board()
    for (num, row, col) in clues[:min_clues]:
        new_puzzle.set_cell(num, row, col)
    make_satisfactory(new_puzzle)

    return new_puzzle
Beispiel #7
0
    def test_similar_puzzle(self):
        # Test that it fails when the puzzle has fewer clues than
        # `min_clues`
        similar_puzzle = generator.similar_puzzle(Board(), self._random_seed(), min_clues=17)
        self.assertFalse(similar_puzzle)

        # Test that the similar puzzle has at least `min_clues` from
        # original
        original_puzzle = generator.solved_puzzle(self._random_seed())
        original_clues = set(original_puzzle.clues())
        min_seed = generator.random_seed()
        # Small to keep the test short
        test_seed_count = 5
        for seed in range(min_seed, min_seed + test_seed_count + 1):
            min_clues = np.random.randint(17, 34)
            similar_puzzle = generator.similar_puzzle(original_puzzle, seed, min_clues=min_clues)
            similar_clues = set(similar_puzzle.clues())
            self.assertGreaterEqual(len(original_clues.intersection(similar_clues)), min_clues)
Beispiel #8
0
def solved_puzzle(seed):
    """Return a random, solved puzzle generated from the provided seed.

    Parameters
    ----------
    seed : int, long, or hashable
        The seed to use for generating the solved board.

    Returns
    -------
    Board instance
        The solved puzzle generated from the given seed.

    """
    # Decreasing this allows for more randomness but can also increase the
    # time required to generate from some seeds
    min_start_clues = 3

    random.seed(seed)
    target_row = random.choice(Board.SUDOKU_ROWS)
    columns = Board.SUDOKU_COLS[:]
    random.shuffle(columns)

    # `numpy.random` excludes upper limit; builtin `random` includes it
    upper_limit_adjustment = 1 if using_numpy_random() else 0
    column_count = random.randint(min_start_clues,
                                  len(columns) + upper_limit_adjustment)

    # Initialize a blank board
    puzzle = Board(name=str(seed))

    # To get the solver started and insert randomness
    for i, target_col in enumerate(columns[:column_count]):
        target_number = Board.SUDOKU_NUMBERS[i]
        puzzle.set_cell(target_number, target_row, target_col)

    solver = Solver(puzzle)
    solver.autosolve_without_history()

    return puzzle
Beispiel #9
0
    def test_minimize(self):
        # Test that it removes no clues if `threshold` is below 17
        original_puzzle = generator.solved_puzzle(self._random_seed())
        minimized_puzzle = original_puzzle.duplicate()
        clues_removed = generator.minimize(minimized_puzzle, threshold=16)
        self.assertFalse(clues_removed)

        # Test that it removes no clues if puzzle's clue count is <=
        # `threshold`
        original_puzzle = Board()
        minimized_puzzle = original_puzzle.duplicate()
        clues_removed = generator.minimize(minimized_puzzle, threshold=17)
        self.assertFalse(clues_removed)

        # Test that it minimizes puzzle and returns difference of clue
        # count
        for seed in self.MINIMIZABLE_SEEDS:
            original_puzzle = generator.solved_puzzle(seed)
            minimized_puzzle = original_puzzle.duplicate()
            clues_removed = generator.minimize(minimized_puzzle)
            self.assertLess(clues_removed, 0)
            clue_difference = minimized_puzzle.clue_count() - original_puzzle.clue_count()
            self.assertEqual(clues_removed, clue_difference)
Beispiel #10
0
    def test_explain(self):
        # So the toggles below work as expected
        self.assertFalse(self.options.markview)

        # Set up main controller
        command_queue = []

        temp_solver = Solver(self.puzzle.duplicate())
        self.assertTrue(temp_solver.autosolve())

        command_queue.append('# Test `explain`ing the initial board')
        command_queue.append(self.EXPLAIN_CMD)
        command_queue.append(
            '# Make explain run automatically after each step')
        command_queue.append('set explainsteps')

        seen_move_types = set()
        command_queue.append(
            '# Test `explain`ing every `step` and `stepm` until solved')
        for num, row, col, _, move_type in temp_solver.move_history:
            new_move_type = move_type not in seen_move_types
            if new_move_type:
                command_queue.append(
                    '# Make sure this type of move displays properly in markview'
                )
                command_queue.append('set markview # turn markview on')
            command_queue.append('step')
            # +1 to correct zero-indexed row, col from `temp_solver`
            command_queue.append('stepm {} {} {}'.format(
                row + 1, col + 1, num))
            if new_move_type:
                command_queue.append('set markview # turn markview off')
                seen_move_types.add(move_type)
        command_queue.append('quit')

        # Set up second controller for testing fringe cases
        fringe_puzzle = Board(name='blank board')
        fringe_command_queue = ['# Test some fringe cases']
        fringe_command_queue.append('# Try to explain unreasonable move')
        fringe_command_queue.append('stepm 5 5 9')
        fringe_command_queue.append('{}  # No reason for move'.format(
            self.EXPLAIN_CMD))
        fringe_command_queue.append(
            '# Try to explain step that removes a clue')
        fringe_command_queue.append('stepm 5 5 0')
        fringe_command_queue.append('{}  # Just reprint board'.format(
            self.EXPLAIN_CMD))
        fringe_command_queue.append('quit')

        # Run commands
        controller = SolverController(self.puzzle,
                                      command_queue=command_queue,
                                      options=self.options)
        fringe_controller = SolverController(
            fringe_puzzle,
            command_queue=fringe_command_queue,
            options=self.options)
        self.redirect_output()
        controller.solve()
        # Turn off option set by previous controller
        fringe_controller.options.explainsteps = False
        fringe_controller.solve()
        self.reset_output()

        if self.compare_file is not None:
            self.output_file.seek(0)
            output_lines = self.output_file.read().splitlines()
            compare_lines = self.compare_file.read().splitlines()
            self.assertEqual(output_lines, compare_lines)