示例#1
0
    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
示例#2
0
 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
示例#3
0
    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))
示例#5
0
 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
示例#6
0
    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, '')
示例#7
0
    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
示例#8
0
 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
示例#9
0
    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])
示例#10
0
    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
示例#11
0
    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
示例#12
0
    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)
示例#13
0
    "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"
)