def test_play_all_games(self): """Plays multiple games in test data at different puzzle sizes""" for i, puz in enumerate(TEST_PUZZLE_STRINGS): with self.subTest(f"Puzzle {i} (len={len(puz)})"): p = su.SudokuPuzzle(starting_grid=ls.from_string(puz)) s = su.SudokuPuzzle( starting_grid=ls.from_string(TEST_SOLUTION_STRINGS[i])) self.assertFalse(p.is_solved()) self.assertTrue(s.is_solved()) for m in p.next_empty_cell(): p.set(*m, s.get(*m)) self.assertTrue(p.is_solved()) return
def test_all_sample_puzzles(self): """Loads all the sample puzzles to check for formatting and validity""" for puz in su.SAMPLE_PUZZLES: with self.subTest(puz["label"]): self.p.init_puzzle(ls.from_string(puz["puzzle"])) self.assertTrue(self.p.is_valid()) return
def run_single_test(self, test_puzzle, solver): """Run a single test case. Method will create a new instance of a puzzle, using the puzzle_class passed on initialization. This method is called by run_tests. The method will check that the "solved" puzzle bears at least a passing resemblence to the original puzzle, so the solver can't "cheat" by just over-writing all cells with a preset pattern. Args: test_puzzle: String containing test puzzle, will be converted using puzzle.latinsquare.from_string. solver_instance: Instance of a solver class with a solve() method. Returns: True if puzzle was solved by solver """ # Initialize puzzle, and make a copy for checking with later puz = self.puzzle_class(starting_grid=ls.from_string(test_puzzle)) orig = copy.deepcopy(puz) # Call solver and check for cheating claimed_solved = solver.solve(puz) if claimed_solved and has_same_clues(orig, puz): self._last_was_solved = puz.is_solved() elif not self.__anti_cheat_check: self._last_was_solved = puz.is_solved() else: self._last_was_solved = False return self._last_was_solved
def test_has_same_clues(self): """We can verify a solution is derived from a puzzle""" for i, puz in enumerate(TEST_PUZZLE_STRINGS): pzzl = ls.LatinSquare(starting_grid=ls.from_string(puz)) soln = ls.LatinSquare( starting_grid=ls.from_string(SOLVED_PUZZLE_STRINGS[i])) self.assertTrue(pt.has_same_clues(pzzl, soln)) self.assertFalse(pt.has_same_clues(soln, pzzl)) # Can handle empty puzzles empty_puzzle = ls.LatinSquare(grid_size=ls.DEFAULT_PUZZLE_SIZE) self.assertTrue(pt.has_same_clues(empty_puzzle, pzzl)) self.assertFalse(pt.has_same_clues(pzzl, empty_puzzle)) # Can handle mismatched sizes small_puzzle = ls.LatinSquare(grid_size=ls.DEFAULT_PUZZLE_SIZE - 1) self.assertFalse(pt.has_same_clues(empty_puzzle, small_puzzle))
def test_puzzle_sizes(self): """Different puzzle sizes are supported""" # Initialise all test puzzles, at different sizes for i, puz in enumerate(TEST_PUZZLE_STRINGS): with self.subTest(f"Test Puzzle {i} init (len={len(puz)})"): p = su.SudokuPuzzle(starting_grid=ls.from_string(puz)) self.assertTrue(p.is_valid()) self.assertEqual(len(puz), p.num_cells) return
def test_from_string(self): """Convert strings to 2D arrays with useful error messages""" with self.subTest("Properly formed strings working"): self.assertEqual(SOLVED_PUZZLE, ls.from_string(SOLVED_STRING)) self.assertEqual([[None]], ls.from_string('.')) self.assertEqual([[1]], ls.from_string('1')) for i in [1, 4, 9, 16, 25]: with self.subTest(f"String length {i}"): self.assertEqual(ls.build_empty_grid(i), ls.from_string('.' * i**2)) with self.subTest("Badly formed strings raise exception"): self.assertRaises(ValueError, ls.from_string, TEST_STRING[0:-1]) self.assertRaises(ValueError, ls.from_string, '.' * (25**2 - 1)) self.assertRaises(ValueError, ls.from_string, '2') self.assertRaises(ValueError, ls.from_string, '1223') self.assertRaises(ValueError, ls.from_string, '')
def test_all_solvers_all_sizes(self): """Solvers can solve different sizes of puzzles""" for m in su.SOLVERS: solver = su.SudokuSolver(method=m) for i, puz in enumerate(TEST_PUZZLE_STRINGS): # Skip backtracking on larger puzzles if m == 'backtracking' and len(puz) > 81: continue with self.subTest(f"Method {m}; Puzzle {i} (len={len(puz)})"): p = su.SudokuPuzzle(starting_grid=ls.from_string(puz)) s = su.SudokuPuzzle( starting_grid=ls.from_string(TEST_SOLUTION_STRINGS[i])) self.assertTrue(p.is_valid()) self.assertFalse(p.is_solved()) self.assertTrue(solver.solve(p)) self.assertTrue(p.is_solved()) self.assertEqual(str(s), str(p)) return
def test_all_solvers_all_puzzles(self): """Test that all available solvers can solve all test puzzles in sudoku.py""" for x in su.SOLVERS: if x == 'backtracking': continue solver = su.SudokuSolver(method=x) for p in su.SAMPLE_PUZZLES: with self.subTest(f"Method {x} on puzzle {p['label']}"): puz = su.SudokuPuzzle( starting_grid=ls.from_string(p['puzzle'])) self.assertTrue(solver.solve(puz)) self.assertTrue(puz.is_solved()) return
def test_init_puzzle(self): """Initialize puzzle with starting clues""" with self.subTest("Using unsolved puzzles"): for i in [TEST_PUZZLE, ls.from_string(TEST_STRING)]: # Init existing puzzle instance self.p.init_puzzle(i) self.assertEqual(50, self.p.num_empty_cells()) self.assertEqual(self.p.max_value**2, self.p.num_cells) self.assertTrue(self.p.is_valid()) self.assertFalse(self.p.is_solved()) # Init on creation newp = ls.LatinSquare(starting_grid=i) self.assertEqual(50, newp.num_empty_cells()) self.assertEqual(self.p.max_value**2, newp.num_cells) self.assertTrue(newp.is_valid()) self.assertFalse(newp.is_solved()) with self.subTest("Using solved puzzles"): for i in [SOLVED_PUZZLE, ls.from_string(SOLVED_STRING)]: # Init existing puzzle instance self.p.init_puzzle(i) self.assertEqual(0, self.p.num_empty_cells()) self.assertEqual(self.p.max_value**2, self.p.num_cells) self.assertTrue(self.p.is_valid()) self.assertTrue(self.p.is_solved()) # Init on creation newp = ls.LatinSquare(starting_grid=i) self.assertEqual(0, newp.num_empty_cells()) self.assertEqual(newp.max_value**2, newp.num_cells) self.assertTrue(newp.is_valid()) self.assertTrue(newp.is_solved()) with self.subTest("Bad puzzle init"): for i in [TEST_STRING, ls.from_string(SOLVED_STRING)]: self.assertRaises(ValueError, self.p.init_puzzle, i[0:-1])
def test_all_solvers_multisolution_puzzles(self): """Test all solvers on how they handle puzzles with multiple solutions""" for m in su.SOLVERS: solver = su.SudokuSolver(method=m) for i, puz in enumerate(MULTI_SOLUTION_STRINGS): with self.subTest(f"Method {m}; Multi-solution puzzle {i}"): # Failure here would be test data error self.p.init_puzzle(ls.from_string(puz)) self.assertTrue(self.p.is_valid()) self.assertFalse(self.p.is_solved()) # Requirement is to return *a* solution self.assertTrue(solver.solve(self.p)) self.assertTrue(self.p.is_solved()) # TODO: Mechanism to report multiple solutions? SAT could # do it. Others might take too long. return
def test_all_solvers_unsolvable_puzzles(self): """Test all solvers on how they handle unsolvable puzzles""" for m in su.SOLVERS: solver = su.SudokuSolver(method=m) for i, puz in enumerate(UNSOLVABLE_STRINGS): with self.subTest(f"Method {m}; Unsolvable puzzle {i}"): # These "unsolvable" puzzles are still valid initially self.p.init_puzzle(ls.from_string(puz)) self.assertTrue(self.p.is_valid()) self.assertFalse(self.p.is_solved()) # Check method correctly reports it cannot be solved self.assertFalse(solver.solve(self.p)) # Solver should leave puzzle in valid, but unsolved state self.assertTrue(self.p.is_valid()) self.assertFalse(self.p.is_solved()) return
def __init__(self, grid_size=None, starting_grid=None): # Convert starting_grid if isinstance(starting_grid, str): starting_grid = from_string(starting_grid) # If both parameters are passed, they need to be consistent if grid_size and starting_grid: if len(starting_grid) != grid_size: raise ValueError( f"starting_grid is not {grid_size}x{grid_size}") elif starting_grid: grid_size = len(starting_grid) elif grid_size is None: grid_size = DEFAULT_SUDOKU_SIZE # Box is square root, check that grid_size is also a square number self.box_size = int(grid_size**(1 / 2)) if self.box_size**2 != grid_size: raise ValueError(f"grid_size={grid_size} is not a square number") # Start by initialising LatinSquare super, use it to calculate # the box size. starting_grid has to be blank, because we're not ready # to set the box constraints yet. blank_grid = build_empty_grid(grid_size) super().__init__(grid_size=grid_size, starting_grid=blank_grid) # Super has initialised row and column constraints. Sudoku puzzles # have an extra constraint -- boxes cannot contain repeated values. self.__allowed_values_for_box = [ set(self.complete_set) for i in range(grid_size) ] # Now it's safe to copy in the starting_grid, which will update the # constraints on rows, columns, boxes if starting_grid: self.init_puzzle(starting_grid)
"387524961124639875569817432835492617796185243412763589673258194248971356951346728", "498157632137682495526439178671348529359216847842795316763524981915873264284961753", # 16x16 "G71C4D6F92E3A58BAF638G297B5CED14845EB73CD1A6G9F229BDA51EG84F6C73613AG4E857CBDF29BCD732A1FG948E56F28GC95D1E6A4B379E45F67B83D2C1AGCDG12B864F753A9E7A2BE1C43DG9F86538945AFG6C2E17BD56EFD397BA182G4C4B786FDAC53G92E1EGC29845A6B173DFD3A91CB2E4F756G815F67EG3298DB4CA", "67A8D4B9CF315GE2915DA876GB2ECF34GFB3E21C547DA9862C4EF53G68A91DB7FBG4C6231785EAD9EAC25DF196GB34788D719A4BECF3265G36958EG72D4AFBC1D52G4B9A3E1768FCBE6A71C2FGD84593C937GFD8A564B21E481F635EB29CD7GA74EB396FDACG8125A28C1GED495F736B1GF6B7A583E29C4D53D92C8471B6GEAF", "12A73G68FB59D4CE54E372C1GA8D96FB6GF8ED9BC724A513DCB95F4A13E687G235CA8BF2E4916GD79E7241D6B8GF3CA5FD14A5G3726CE8B9B68GCE795D3A2F41C325G7B4D61EFA988FD19AE54G73B26CEA46283C9FB5G17DG79BD61F8CA25E3478GCF4AE35DB19262B3F19576E4GCD8AA96EBC8D21F7435G415D632GA9C87BEF", # 25x25 "HDPMKJ546F8COE1I2NL3B9G7AFJE481DI325LNGACM9B7H6KPOO9CLBM7ENA6KHFJ485PG1I23DI2G6A8BKPO947D3FEHJ1NLC5MN5173G9HCLIMB2PKD6OAFE4J84O8NEIJM9KP2AHF76G3CL51DBL6MDGPA3OENI4JB591KFCH782PKI2HB6L14M857CJNDAE3OFG91ABJ57GDFC39E6LOH82MPKIN49C73F28NH51DKOGL4BIP6AMEJM1OC6DP5LNGJ29KH7AF8IB34EGFA8LHK72MO6I54NB3EJ9PDC1DHNB7CO14GAEF3MP5I92J86LKE359I6F8BJ7PCLDGO41K2MHAN2PJK49IAE3HB81N6CMDLOG5F7BMKI2LN67HF13AO8GJ5D4CE9PJGDHC4MFKB25P8791ENOA3LI6A8651OEGDPBH9KI3L7C4MJN2F34FENA29J1DGLC6BKPMI87OH57L9OP53C8IJNM4EAF26HKDB1G6E4PDNLBG8CO1IHMAF7952JK35NL1OE4PI9K7JB823CG6DFAMHC7HFJ31OMD4A6P2EIK85GN9BL8I3AMKH257LFGN9DJO4BE1P6CKB2G9FCJA6E3DM51PLHN748OI", "HAJBP7C1MOE4KN25LDFGI936897D3EFJIN86OLH5142PCAGKMB56KGFLB9EPJ813DHNMIA247CO1OIC45GAH27B9MPE38K6JDLFNM2L8ND6K43GCIAF97JOBP1H5EA1OMJ3NEBCFG8L4P9625DKIH748FK9P15ADMECOJ3BIH762NGLNGPIHOF76J9AD2BKMLCE35841B57DCGLH2IK3N16OJF48MEAP92LE634K89MI75PHA1GDNBFJOCLM8NA93J7E1KBFG2CP5OHI6D4J45O1BM28NLH7DC6EA9IG3FKPIE39DA5CLH86PJN7FKG4OM1B2GF276I4P1K5MO93BHNJDC8ELACPBHK6ODGF42EIAM813LNJ9753KH2MCEGP6ONFB84A5LJ17D9IPNC578DM312IAK9FOE6H4LBJGD9ALOK24I5HPJ71NG3BME6C8FFJ14BNHLO9D5G6E8IC72KPMA38I6EGJABF73L4CMDP91K5O2NHOC4J2MPFDAN13E7G6B89LH5IK7HNFL286KBPDMGIC54E39AO1J63GA819OC4BJ25LIKHMF7NPEDED9PIH7N5LCF64KJ2OA18BG3MKBM15EI3JGA9H8OLD7NPFC426", "P9NKJ2B461D5FC8GAH3LMOEI7ECFI3OK8HAG7L92D51JM4PN6BB5DO7NCLPGJ13M48FEI6H9A2K61HGA7FDMEIPBOK24NC9JL385L824MI3J59HNE6APKB7OD1FCG7IB3294KFMLDAG518PONC6JEHK61DC8L57PM4I3BE2JFHONGA9HFLM86NOJDC927EKI5AG14BP34GONECHA3BKFPJ1697LD8MI52APJ591EIG2O8NH6CB3M4F7LKDG4I95L1NECBJOKDF7A283HPM63A861JOP27EM4NG9DKHCIF5BLFMCEOB96K35IHLP4JGN1A2D782BPHKDGF4I837A9M6LE5NJCO1DJ7LNMAH8526C1F3POBIGK94ENHKPBADC9LFG84J5EM6273O1IOD48IH72BFAKMPCLG913E56JNM2E165J3I47O9DLANF8KBCHGPC3AFLG6MN81E5BHJOIP7KD294579JGEP1OK326INHC4DBL8MFA1KMCFP8BA6NLDEI7H25J9G43OJN5243I71H9BGFMOL6KAPE8DCILG7P45ECO6H183BMD9F2AKNJ9O6AHKMGDNPCJ27I384E5B1LF8E3BDF29LJ4AK5ON1CGP6I7HM", ] EASY_PUZZLE = ls.from_string( "89.4...5614.35..9.......8..9.....2...8.965.4...1.....5..8.......3..21.7842...6.13" ) EASY_SOLUTION = ls.from_string( "893472156146358792275619834954183267782965341361247985518734629639521478427896513" ) EASY_MOVES_LEGAL = [[0, 2, 3], [3, 3, 1], [4, 4, 6], [1, 6, 7], [7, 3, 5], [1, 8, 2], [8, 2, 7]] EASY_MOVES_ILLEGAL = [[0, 2, 8], [0, 2, 1], [3, 3, 2], [3, 3, 4], [2, 2, 4], [3, 3, 6], [0, 0, 9]] HARD_PUZZLE = ls.from_string( "..8......1..6..49.5......7..7..4.....5.2.6...8..79..1..63.....1..5.73......9..75." ) HARD_SOLUTION = ls.from_string( "498157632137682495526439178671348529359216847842795316763524981915873264284961753" )