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
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
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
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
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
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
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)
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
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)
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)
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)
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)