Esempio n. 1
0
def make_satisfactory(puzzle):
    """Add all clues that seem to require guessing; return amount added.

    Parameters
    ----------
    puzzle : Board instance
        The puzzle to make satisfactory by adding in non-deducible moves.

    Returns
    -------
    int
        The difference between the puzzle's new clue count and its old.

    See Also
    --------
    minimize : a method that makes puzzles harder by removing clues.

    """
    clues_added = 0
    solver = Solver(puzzle.duplicate())
    solver.autosolve()
    for move in solver.guessed_moves():
        puzzle.set_cell(*move)
        clues_added += 1
    return clues_added
Esempio n. 2
0
    def test_make_rotationally_symmetric(self):
        for seed in self.MINIMIZABLE_SEEDS:
            puzzle = generator.generate(seed)
            prev_clue_count = 0
            for minimized in [False, True]:
                iter_info = 'for seed={} and minimized={}'.format(seed, minimized)
                symmetric_puzzle = puzzle.duplicate()
                # Test that the reported clue difference is correct
                clue_difference = generator.make_rotationally_symmetric(symmetric_puzzle,
                                                                        minimized=minimized)
                expected_clue_difference = symmetric_puzzle.clue_count() - puzzle.clue_count()
                assertion_msg = '{} != {} {}'.format(clue_difference, expected_clue_difference,
                                                     iter_info)
                self.assertEqual(clue_difference, expected_clue_difference, assertion_msg)

                clue_count = symmetric_puzzle.clue_count()
                if prev_clue_count:
                    # Test that the minimized puzzle has no more clues than
                    # the symmetric one (NB `MINIMIZABLE_SEEDS` are not
                    # necessarily minimizable while also maintaining
                    # symmetry)
                    assertion_msg = '{} > {} {}'.format(clue_count, prev_clue_count, iter_info)
                    self.assertLessEqual(clue_count, prev_clue_count, assertion_msg)
                    prev_clue_count = 0
                prev_clue_count = 0 if minimized else clue_count

                # Test that the puzzle is actually symmetric
                rows = list(map(tuple, np.array(self._binarize_rows(symmetric_puzzle.rows()))))
                rot_rows = list(map(tuple, np.rot90(rows, k=-2)))
                assertion_msg = '{} != {} {}'.format(rows, rot_rows, iter_info)
                self.assertEqual(rows, rot_rows, assertion_msg)

        puzzle = generator.generate(self.NONSATISFACTORY_SEEDS[0], minimized=True)
        satisfactory_puzzle = puzzle.duplicate()
        clue_difference = generator.make_satisfactory(satisfactory_puzzle)
        puzzle_differences = puzzle.differences(satisfactory_puzzle)
        # Test that clues were added
        self.assertGreater(len(puzzle_differences), 0)
        # Make puzzle symmetric while keeping it satisfactory
        generator.make_rotationally_symmetric(satisfactory_puzzle, minimized=True,
                                              keep_satisfactory=True)

        # Test that the puzzle is now actually symmetric
        rows = list(map(tuple, np.array(self._binarize_rows(satisfactory_puzzle.rows()))))
        rot_rows = list(map(tuple, np.rot90(rows, k=-2)))
        self.assertEqual(rows, rot_rows)

        # Test that the puzzle is still satisfactory
        solver = Solver(satisfactory_puzzle)
        solver.autosolve()
        self.assertFalse(len(solver.guessed_moves()))
Esempio n. 3
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
Esempio n. 4
0
    def test_make_satisfactory(self):
        # Test that the return value is 0 when nothing can be added
        solved_puzzle = generator.solved_puzzle(self._random_seed())
        clues_added = generator.make_satisfactory(solved_puzzle)
        self.assertFalse(clues_added)

        for seed in self.NONSATISFACTORY_SEEDS:
            original_puzzle = generator.generate(seed, minimized=True)
            satisfactory_puzzle = original_puzzle.duplicate()

            # Test that it added clues
            clues_added = generator.make_satisfactory(satisfactory_puzzle)
            self.assertGreater(clues_added, 0)

            # Test that the reported number of clues added is accurate
            clue_difference = satisfactory_puzzle.clue_count() - original_puzzle.clue_count()
            self.assertEqual(clues_added, clue_difference)

            # Test that it actually eliminated guesses
            solver = Solver(satisfactory_puzzle)
            solver.autosolve()
            self.assertFalse(len(solver.guessed_moves()))
Esempio n. 5
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)
Esempio n. 6
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)
Esempio n. 7
0
def make_rotationally_symmetric(puzzle,
                                minimized=False,
                                keep_satisfactory=False):
    """Give puzzle 180-deg rotational symmetry; return clue count change.

    Add and, if `minimized` is True, remove clues from `puzzle` such that,
    if this new puzzle were rotated 180 degrees, the cells with clues in
    them in the rotated and non-rotated puzzles would be the same.

    Parameters
    ----------
    puzzle : Board instance
        The puzzle to make symmetric by adding or removing clues.
    minimized : bool, optional
        True if clues may be removed from the puzzle provided this can be
        done while preserving symmetry and properness, and False if
        symmetry may only be created by adding clues (default False).
    keep_satisfactory : bool, optional
        Used when `minimized` is True to determine if clues whose removal
        introduce guessing may still be removed (`keep_satisfactory=True`)
        or if that should be prevented (`keep_satisfactory=False`) (default
        False).

    Returns
    -------
    int
        The difference between the puzzle's new clue count and its old.

    """
    clues = puzzle.clues()
    original_clue_count = len(clues)

    original_guess_count = 0
    if keep_satisfactory:
        temp_solver = Solver(puzzle.duplicate())
        temp_solver.autosolve()
        original_guess_count = len(temp_solver.guessed_moves())

    solver = Solver(puzzle.duplicate())
    solver.autosolve_without_history()

    for (num, row, col) in clues:
        rot_row, rot_col = _rotated_location(row, col)
        rot_num = solver.puzzle.get_cell(rot_row, rot_col)

        if not minimized:
            puzzle.set_cell(rot_num, rot_row, rot_col)
            continue

        # See if the puzzle still has a unique solution with the clues at
        # the original location and its rotational partner location removed
        puzzle.set_cell(Board.BLANK, row, col)
        puzzle.set_cell(Board.BLANK, rot_row, rot_col)

        temp_solver = Solver(puzzle.duplicate())
        if temp_solver.solution_count(limit=2) != 1 or keep_satisfactory:
            new_guess_count = 0
            if keep_satisfactory:
                # Check if changes introduced additional guesses
                temp_solver.autosolve()
                new_guess_count = len(temp_solver.guessed_moves())

            if not keep_satisfactory or new_guess_count > original_guess_count:
                # Puzzle no longer has a unique solution or now has more
                # guesses than before; undo changes
                puzzle.set_cell(num, row, col)
                puzzle.set_cell(rot_num, rot_row, rot_col)

    return puzzle.clue_count() - original_clue_count
Esempio n. 8
0
def minimize(puzzle, threshold=17):
    """Remove clues not needed for a unique solution; return amount removed.

    Remove all clues from `puzzle` that are not needed for it to have a
    single solution (provided it's initial clue count is above
    `threshold`).

    Parameters
    ----------
    puzzle : Board instance
        The puzzle from which to remove unnecessary clues.
    threshold : int, optional
        Puzzles with their number of clues less than or equal to this value
        will not be minimized further (default 17).

    Returns
    -------
    int
        The difference between the puzzle's new clue count and its old.

    See Also
    --------
    make_satisfactory : a method that makes puzzles easier by adding clues.

    Notes
    -----
    The default for `threshold` is 17 because that's the minimum number of
    clues needed to guarantee a unique solution to the puzzle.[1]_ This
    function does not check that the minimized puzzle has the same solution
    as the original one, but that outcome should be inevitable: the
    function only ever removes clues, and removing a clue from a Sudoku
    will never remove a solution from the solution set; at most, doing so
    adds one or more new solutions, and since the function undoes any
    removals that increase the size of the solution set, the solution sets
    of the original and the minimized puzzle have to be the same.

    References
    ----------
    .. [1] G. McGuire, B. Tugemann and G. Civario, "There Is No 16-Clue
    Sudoku: Solving the Sudoku Minimum Number of Clues Problem via
    Hitting Set Enumeration", Experimental Mathematics, vol. 23, no. 2,
    pp. 190-217, 2012. Available at: https://arxiv.org/abs/1201.0749
    [Accessed 25 Jun. 2017].

    """
    original_clue_count = puzzle.clue_count()

    if threshold < 17 or original_clue_count <= threshold:
        return 0

    solver = Solver(puzzle)
    while True:
        clues_removed = 0
        for (num, row, col) in puzzle.clues():
            puzzle.set_cell(Board.BLANK, row, col)
            if solver.solution_count(limit=2) != 1:
                # Multiple solutions after this change, so reset
                puzzle.set_cell(num, row, col)
            else:
                clues_removed += 1
        if not clues_removed:
            break

    return puzzle.clue_count() - original_clue_count
Esempio n. 9
0
def main():
    """Get puzzle(s) from file, user, or seed and solve each in turn.

    """
    args = _get_parser().parse_args()

    # Warn if the UTF-8 output will look like garbage on this terminal
    if not args.ascii and sys.stdout.encoding in ['ascii', 'ANSI_X3.4-1968']:
        error.error(
            'assuming --ascii since your terminal does not seem to'
            ' support UTF-8 output. To fix permanently, please set'
            " your locale environment variables and your terminal's"
            ' character encoding settings appropriately. To force'
            ' UTF-8 output temporarily, try calling this program'
            ' using "PYTHONIOENCODING=utf_8 sudb" instead of just'
            ' "sudb".\n',
            prelude='warning')
        args.ascii = True

    init_commands = []
    if not args.no_init:
        init_path = os.path.expanduser(INIT_FILE)
        try:
            with open(init_path, 'r') as init:
                init_commands = [
                    line for line in init.read().split('\n') if line
                ]
        except IOError:
            pass

    command_queue = [] if args.execute is None else args.execute

    # Flatten the list of puzzle lines
    lines = None if args.lines is None else [
        line for line_list in args.lines for line in line_list
    ]

    if args.random is not None and not args.random:
        # `-r` was used without arguments
        args.random.append(generator.random_seed())

    log = PuzzleErrorLogger()
    puzzles = importer.get_puzzles(filenames=args.file,
                                   lines=lines,
                                   seeds=args.random,
                                   logger=log)
    solved_puzzles = 0

    if log.error_count() > 0:
        # There was an import error; print a newline to stderr for spacing
        error.error('', prelude='')
    print('{} puzzle{}to solve.'.format(len(puzzles),
                                        's ' if len(puzzles) != 1 else ' '))

    for i, puzzle in enumerate(puzzles):
        _edit_puzzle(puzzle, args.satisfactory, args.minimized,
                     args.symmetrical)

        skip_solving = False
        if log.autolog(puzzle, report=True) and log.in_mask(
                puzzle, log.unsolvable_mask()):
            # A unsolvable error occured
            skip_solving = True

        # A copy of the original is needed to find differences between it
        # and the final state of the board and for error output
        original_puzzle = puzzle.duplicate()

        if skip_solving:
            pass
        elif args.auto:
            auto_solver = Solver(puzzle)
            if auto_solver.autosolve_without_history():
                solved_puzzles += 1
        else:
            options = SolverController.Options()
            options.ascii = args.ascii
            solver = SolverController(puzzle,
                                      init_commands=init_commands,
                                      command_queue=command_queue,
                                      options=options)
            try:
                if solver.solve():
                    solved_puzzles += 1
            except BaseException:
                # To make it easier to return to board state at crash
                print('\nFinal State of Puzzle {} ({}):'.format(
                    i + 1, puzzle.name))
                print(puzzle)
                print()
                with open(CMDHIST_FILE, 'w+') as cmdhist:
                    cmdhist.write('\n'.join(solver.command_history))
                puzzle_line_args = ' '.join(str(original_puzzle).split())
                print('Command history saved in "{}". To restore:'.format(
                    CMDHIST_FILE))
                print('{} -x "source {}" -l {}'.format(sys.argv[0],
                                                       CMDHIST_FILE,
                                                       puzzle_line_args))
                print()
                raise

        colormap = None
        if args.difference:
            colormap = frmt.get_colormap(puzzle.differences(original_puzzle),
                                         frmt.Color.GREEN)
        puzzle_str = frmt.strfboard(puzzle,
                                    colormap=colormap,
                                    ascii_mode=args.ascii)

        print('\nEnd Board for Puzzle {}:'.format(i + 1))
        print('{}\n({})\n'.format(puzzle_str, puzzle.name))

    print('Solved {} of {}.'.format(solved_puzzles, len(puzzles)))
    if log.error_count() > 0:
        print()
        log.print_summary()
Esempio n. 10
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)