예제 #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
예제 #2
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
예제 #3
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
예제 #4
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
예제 #5
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
예제 #6
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
예제 #7
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)
예제 #8
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
예제 #9
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)
예제 #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)
예제 #11
0
    def test_step(self):
        command_queue = []

        available_first_moves = []
        solver = Solver(self.puzzle)
        for (row, col) in Board.SUDOKU_CELLS:
            candidates = solver.candidates(row, col)
            if len(candidates) == 1 and self.puzzle.get_cell(
                    row, col) == Board.BLANK:
                # Solver uses zero-indexed locations, so correct
                available_first_moves.append(
                    (row + 1, col + 1, candidates.pop()))

        command_queue.append('# test `step` variants')
        for i, (row, col, num) in enumerate(available_first_moves[:2]):
            command_queue.append('# try to deduce {} at ({}, {})'.format(
                num, row, col))
            box, _ = Board.box_containing_cell(row - 1, col - 1)
            box += 1
            # Alternate the command variation used each iteration
            command_queue.append('{} {}'.format(self.STEPR_CMDS[i % 2], row))
            command_queue.append(self.UNSTEP_CMD)
            command_queue.append('{} {}'.format(self.STEPC_CMDS[i % 2], col))
            command_queue.append(self.UNSTEP_CMD)
            command_queue.append('{} {}'.format(self.STEPB_CMDS[i % 2], box))
            command_queue.append(self.UNSTEP_CMD)
            command_queue.append('# set {} at ({}, {}) manually'.format(
                num, row, col))
            command_queue.append('{} {} {} {}'.format(self.STEPM_CMDS[i % 2],
                                                      row, col, num))
            command_queue.append(self.UNSTEP_CMD)
            command_queue.append('# using `stepm` ROW COL NUM alias')
            command_queue.append('{1}{0}{2}{0}{3}'.format(
                ' ' * (i % 2), row, col, num))
            command_queue.append(self.UNSTEP_CMD)

        command_queue.append('# test breaking on `step`s')
        for step_cmd, row, col in self.FIRST_MOVES:
            command_queue.append('{} {} {}'.format(self.BREAK_CMD, row, col))
            command_queue.append(step_cmd)
            command_queue.append('# breakpoint at ({}, {})'.format(row, col))
            command_queue.append(self.UNSTEP_CMD)
        command_queue.append(self.DELETE_CMD)

        command_queue.append('# test repeating `step`s')
        command_queue.append('{} 2'.format(self.STEP_CMDS[0]))
        command_queue.append('{} 12'.format(self.STEPR_CMDS[0]))
        command_queue.append('{} 12'.format(self.STEPC_CMDS[0]))
        command_queue.append('{} 12'.format(self.STEPB_CMDS[0]))
        command_queue.append('{} 8'.format(self.UNSTEP_CMD))

        command_queue.append(
            '# test passing bad arguments to various commands')
        command_queue.append('# No steps left to undo')
        command_queue.append('{} 82'.format(self.UNSTEP_CMD))
        command_queue.append('# Must be integer')
        command_queue.append('{} x'.format(self.STEP_CMDS[0]))
        command_queue.append('{} x'.format(self.UNSTEP_CMD))
        command_queue.append('{} 1 x'.format(self.STEPR_CMDS[0]))
        command_queue.append('{} 1 x'.format(self.STEPC_CMDS[0]))
        command_queue.append('{} 1 x'.format(self.STEPB_CMDS[0]))
        command_queue.append('{} x x x'.format(self.STEPM_CMDS[0]))
        command_queue.append('# Invalid row')
        command_queue.append('{} 0 1 1'.format(self.STEPM_CMDS[0]))
        command_queue.append('{} 0'.format(self.STEPR_CMDS[0]))
        command_queue.append('# Invalid column')
        command_queue.append('{} 1 0 1'.format(self.STEPM_CMDS[0]))
        command_queue.append('{} 0'.format(self.STEPC_CMDS[0]))
        command_queue.append('# Invalid box')
        command_queue.append('{} 0'.format(self.STEPB_CMDS[0]))
        command_queue.append('# Argument required')
        command_queue.append(self.STEPR_CMDS[0])
        command_queue.append(self.STEPC_CMDS[0])
        command_queue.append(self.STEPB_CMDS[0])
        command_queue.append('# Exactly three arguments required')
        command_queue.append('{}'.format(self.STEPM_CMDS[0]))
        command_queue.append('{} 1'.format(self.STEPM_CMDS[0]))
        command_queue.append('{} 1 1'.format(self.STEPM_CMDS[0]))
        command_queue.append('# Inconsistent move')
        command_queue.append(self.INCONSISTENT_MOVE)

        # Tell controller to exit
        command_queue.append('quit')

        # Run commands
        controller = SolverController(self.puzzle,
                                      command_queue=command_queue,
                                      options=self.options)
        self.redirect_output()
        controller.solve()
        self.reset_output()

        if self.compare_file is None:
            return

        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)
예제 #12
0
    def test_set(self):
        temp_solver = Solver(self.puzzle.duplicate())
        move1_row, move1_col = temp_solver.step()
        move1_box, _ = Board.box_containing_cell(move1_row, move1_col)
        move1_num = temp_solver.puzzle.get_cell(move1_row, move1_col)

        # Adjust zero-indexed locations returned by `temp_solver`
        move1_row += 1
        move1_col += 1
        move1_box += 1

        command_queue = []
        command_queue.append('# test `set` with no subcommand')
        command_queue.append(self.SET_CMD)

        command_queue.append('# test `set ascii`')
        command_queue.append('{} ascii # turn on'.format(self.SET_CMD))
        command_queue.append('print')
        command_queue.append('{} ascii # turn off'.format(self.SET_CMD))
        command_queue.append('print')

        command_queue.append('# test `set guessbreak`')
        command_queue.append(
            'step {} # right before guess'.format(self.MOVE_OF_FIRST_GUESS -
                                                  1))
        command_queue.append('check move_before_guess')
        command_queue.append('step 2')
        command_queue.append('restart move_before_guess')
        command_queue.append('{} guessbreak # turn on'.format(self.SET_CMD))
        command_queue.append('step 2')
        command_queue.append('restart')
        command_queue.append('{} guessbreak # turn off'.format(self.SET_CMD))

        command_queue.append('# test `set explainsteps`')
        command_queue.append('{} explainsteps # turn on'.format(self.SET_CMD))
        command_queue.append(
            '# make sure explanation is printed for `step`/`stepm`')
        command_queue.append('step')
        command_queue.append('stepm {} {} {}'.format(move1_row, move1_col,
                                                     move1_num))
        command_queue.append('unstep 2')
        command_queue.append(
            '# make sure explanation is printed for other `step` variants')
        command_queue.append('stepr {}'.format(move1_row))
        command_queue.append('unstep')
        command_queue.append('stepc {}'.format(move1_col))
        command_queue.append('unstep')
        command_queue.append('stepb {}'.format(move1_box))
        command_queue.append('unstep')
        command_queue.append('# make sure explanation is printed for `finish`')
        command_queue.append('break {} {} # avoid excess output'.format(
            move1_row, move1_col))
        command_queue.append('finish')
        command_queue.append('{} explainsteps # turn off'.format(self.SET_CMD))

        command_queue.append('# test `set markview`')
        command_queue.append('{} markview # turn on'.format(self.SET_CMD))
        command_queue.append('mark 1 1 9  # add some candidates')
        command_queue.append('mark 9 9 9')
        command_queue.append('step')
        command_queue.append('explain')
        command_queue.append('unstep')
        command_queue.append('{} markview # turn off'.format(self.SET_CMD))

        command_queue.append('# test `set prompt`')
        command_queue.append('set prompt sudb# ')
        command_queue.append('set prompt all spaces  taken    literally> ')
        command_queue.append('# even no argument is taken literally')
        command_queue.append('set prompt')
        command_queue.append('# reset prompt')
        command_queue.append('set prompt {}'.format(self.options.prompt))

        command_queue.append('# test `set width`')
        command_queue.append('{} width 80 # set wide'.format(self.SET_CMD))
        command_queue.append('print marks  # show wide puzzle')
        command_queue.append('help step    # show wide wrapped text')
        command_queue.append('{} width 60 # set narrow'.format(self.SET_CMD))
        command_queue.append('print marks  # show narrow puzzle')
        command_queue.append('{} ascii'.format(self.SET_CMD))
        command_queue.append('print marks  # show narrow ascii puzzle')
        command_queue.append('help step    # show narrow wrapped text')
        command_queue.append('{} width 0  # restore default'.format(
            self.SET_CMD))

        command_queue.append('# test passing bad arguments')
        command_queue.append('# Undefined set command')
        command_queue.append('{} madeup_set_command'.format(self.SET_CMD))
        command_queue.append('# Argument must be integer')
        command_queue.append('{} width x'.format(self.SET_CMD))
        command_queue.append('# Integer out of range')
        command_queue.append('{} width -1'.format(self.SET_CMD))
        command_queue.append('# One argument required')
        command_queue.append('{} width'.format(self.SET_CMD))

        command_queue.append('# test showing all current settings')
        command_queue.append('info {} # before changes'.format(self.SET_CMD))
        command_queue.append('# make changes')
        command_queue.append('{} ascii'.format(self.SET_CMD))
        command_queue.append('{} explainsteps'.format(self.SET_CMD))
        command_queue.append('{} guessbreak'.format(self.SET_CMD))
        command_queue.append('{} markview'.format(self.SET_CMD))
        command_queue.append('{} prompt (test) '.format(self.SET_CMD))
        command_queue.append('{} width 70'.format(self.SET_CMD))
        command_queue.append('info {} # after changes'.format(self.SET_CMD))

        # Tell controller to exit
        command_queue.append('quit')

        # Run commands
        controller = SolverController(self.puzzle,
                                      command_queue=command_queue,
                                      options=self.options)
        self.redirect_output()
        controller.solve()
        self.reset_output()

        if self.compare_file is None:
            return

        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)