def test_4(solver_status, board, window): """ Suppose both cells in the ceiling contain extra candidates. Suppose the common candidates are U and V, and none of the cells seen by both ceiling cells contains U. Then V can be eliminated from these two cells. A rectangle that meets this test is also called a Type 4 Unique Rectangle. Rating: 100 """ def _get_candidate_count(cells, candidate): return ''.join(board[cell] for cell in cells).count(candidate) def _check_rectangles(by_row): cells_by_x = CELLS_IN_ROW if by_row else CELLS_IN_COL cells_by_y = CELLS_IN_COL if by_row else CELLS_IN_ROW for floor_id_x in range(9): bi_values.clear() for floor_id_y in range(9): cell = cells_by_x[floor_id_x][floor_id_y] if len(board[cell]) == 2: bi_values[board[cell]].add((floor_id_x, floor_id_y, CELL_BOX[cell])) for bi_value, coordinates in bi_values.items(): if len(coordinates) == 2: floor_a = coordinates.pop() floor_b = coordinates.pop() x_ids = _get_xyz(7, board, bi_value, cells_by_y[floor_a[1]], cells_by_y[floor_b[1]]) for x_id in x_ids: ceiling_a = x_id * 9 + floor_a[1] if by_row else floor_a[1] * 9 + x_id ceiling_b = x_id * 9 + floor_b[1] if by_row else floor_b[1] * 9 + x_id boxes = {floor_a[2], floor_b[2], CELL_BOX[ceiling_a], CELL_BOX[ceiling_b]} if len(boxes) == 2: to_eliminate = None cells = set(ALL_NBRS[ceiling_a]).intersection(ALL_NBRS[ceiling_b]) if not _get_candidate_count(cells, bi_value[0]): to_eliminate = {(bi_value[1], ceiling_a), (bi_value[1], ceiling_b)} elif not _get_candidate_count(cells, bi_value[1]): to_eliminate = {(bi_value[0], ceiling_a), (bi_value[0], ceiling_b)} if to_eliminate: rows = (floor_a[0], x_id) if by_row else (floor_a[1], floor_b[1]) columns = (floor_a[1], floor_b[1]) if by_row else (floor_a[0], x_id) rectangle = _get_rectangle(sorted(rows), sorted(columns)) other_candidates = set(board[ceiling_a]).union(board[ceiling_b]).difference(bi_value) c_chain = _get_c_chain(rectangle, bi_value, other_candidates) solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) if window: window.options_visible = window.options_visible.union(c_chain.keys()).union(cells) kwargs["solver_tool"] = "uniqueness_test_4" kwargs["c_chain"] = c_chain kwargs["eliminate"] = to_eliminate test_4.clues += len(solver_status.naked_singles) test_4.options_removed += len(to_eliminate) return True return False set_remaining_candidates(board, solver_status) kwargs = {} bi_values = defaultdict(set) if _check_rectangles(True) or _check_rectangles(False): return kwargs return None
def test_1(solver_status, board, window): """ If there is only one cell in the rectangle that contains extra candidates, then the common candidates can be eliminated from that cell. Rating: 100 """ set_remaining_candidates(board, solver_status) bi_values = _get_bi_values_dictionary(board, range(81)) for bi_value, coordinates in bi_values.items(): if len(coordinates) > 2: for triplet in combinations(coordinates, 3): rows = {position[0] for position in triplet} columns = {position[1] for position in triplet} boxes = {position[2] for position in triplet} if len(rows) == 2 and len(columns) == 2 and len(boxes) == 2: rectangle = _get_rectangle(rows, columns) if all(bi_value[0] in board[corner] and bi_value[1] in board[corner] for corner in rectangle): for corner in rectangle: if len(board[corner]) > 2: to_eliminate = {(candidate, corner) for candidate in bi_value} other_candidates = set(board[corner]).difference(bi_value) c_chain = _get_c_chain(rectangle, bi_value, other_candidates) solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) if window: window.options_visible = window.options_visible.union(rectangle) kwargs = {"solver_tool": "uniqueness_test_1", "c_chain": c_chain, "eliminate": to_eliminate, } test_1.clues += len(solver_status.naked_singles) test_1.options_removed += len(to_eliminate) return kwargs return None
def unique_rectangles_(solver_status, board, window): """Remove candidates (options) using Unique Rectangle technique (see https://www.learn-sudoku.com/unique-rectangle.html)""" # 'pairs' data structure: # {'xy': [(row, col, blk), ...]} # Finding unique rectangles: # - a pair is in at least three cells and the pair values are in options of the fourth one # - the pair is in exactly two rows, to columns and two blocks def _reduce_rectangle(a_pair, corners): if all(board[corner] == a_pair for corner in corners): return False to_eliminate = [] for corner in corners: if board[corner] != a_pair: subset = [cell for cell in rect if len(board[cell]) == 2] if a_pair[0] in board[corner]: to_eliminate.append((a_pair[0], corner)) if a_pair[1] in board[corner]: to_eliminate.append((a_pair[1], corner)) if to_eliminate: solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) if window: window.options_visible = window.options_visible.union(set(corners)) kwargs["solver_tool"] = "unique_rectangles" kwargs["rectangle"] = rect kwargs["eliminate"] = to_eliminate kwargs["subset"] = subset print('\tUniueness Test 1') return True return False set_remaining_candidates(board, solver_status) kwargs = {} pairs = defaultdict(list) for i in range(81): if len(board[i]) == 2: pairs[board[i]].append((CELL_ROW[i], CELL_COL[i], CELL_BOX[i])) for pair, positions in pairs.items(): if len(positions) > 2: rows = list(set(pos[0] for pos in positions)) cols = list(set(pos[1] for pos in positions)) blocks = set(pos[2] for pos in positions) if len(rows) == 2 and len(cols) == 2 and len(blocks) == 2: row_1 = CELLS_IN_ROW[rows[0]] row_2 = CELLS_IN_ROW[rows[1]] rect = sorted([row_1[cols[0]], row_1[cols[1]], row_2[cols[0]], row_2[cols[1]]]) for val in pair: if not all(val in board[corner] for corner in rect): break else: if _reduce_rectangle(pair, rect): return kwargs return {}
def pencil_mark_btn_clicked(window, _, board, *args, **kwargs): """ action on pressing 'Pencil mark' button """ if window.buttons[pygame.K_p].is_active( ) and not window.buttons[pygame.K_p].is_pressed(): window.buttons[pygame.K_p].set_pressed(True) window.buttons[pygame.K_p].draw(window.screen) window.buttons[pygame.K_c].set_pressed(False) window.buttons[pygame.K_c].draw(window.screen) set_remaining_candidates(board, solver_status)
def remote_pairs(solver_status, board, window): """ TODO """ def _find_chain(pair): chain_cells = set(pairs_positions[pair]) ends = [cell_id for cell_id in chain_cells if len(set(ALL_NBRS[cell_id]).intersection(chain_cells)) == 1] inner_nodes = [cell_id for cell_id in chain_cells if len(set(ALL_NBRS[cell_id]).intersection(chain_cells)) == 2] if len(ends) == 2 and ends[0] not in ALL_NBRS[ends[1]] and len(ends) + len(inner_nodes) == len(chain_cells): # print('\n') chain = [ends[0]] while inner_nodes: for node in inner_nodes: if chain[-1] in set(ALL_NBRS[node]): chain.append(node) break if chain[-1] in inner_nodes: inner_nodes.remove(chain[-1]) elif len(chain) % 2 == 0: return [] else: break chain.append(ends[1]) return chain else: return [] set_remaining_candidates(board, solver_status) pairs_positions = defaultdict(list) for cell in range(81): if len(board[cell]) == 2: pairs_positions[board[cell]].append(cell) pair_chains = [pair for pair in pairs_positions if len(pairs_positions[pair]) > 3 and len(pairs_positions[pair]) % 2 == 0] for pair in pair_chains: chain = _find_chain(pair) if chain: impacted_cells = set(ALL_NBRS[chain[0]]).intersection(set(ALL_NBRS[chain[-1]])) to_eliminate = [(value, cell) for value in pair for cell in impacted_cells if value in board[cell] and len(board[cell]) > 1] if to_eliminate: solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union(impacted_cells) eliminate_options(solver_status, board, to_eliminate, window) kwargs = { "solver_tool": "remote_pairs", "chain": chain, "eliminate": to_eliminate, "impacted_cells": impacted_cells, } return kwargs return {}
def test_6(solver_status, board, window): """ Suppose exactly two cells in the rectangle contain extra candidates, and they are located diagonally across each other in the rectangle. Suppose the common candidates are U and V, and none of the other cells in the two rows and two columns containing the rectangle contain U. Then U can be eliminated from these two cells. This is also called a Type 6 Unique Rectangle. Rating: 100 """ set_remaining_candidates(board, solver_status) kwargs = {} bi_values = _get_bi_values_dictionary(board, range(81)) bi_values = {key: value for key, value in bi_values.items() if len(value) > 1} for bi_value in bi_values: for pair in combinations(bi_values[bi_value], 2): if pair[0][0] != pair[1][0] and pair[0][1] != pair[1][1] and pair[0][2] != pair[1][2]: node_b = pair[0][0] * 9 + pair[1][1] node_d = pair[1][0] * 9 + pair[0][1] if len(set(board[node_b]).intersection(bi_value)) == 2 and \ len(set(board[node_d]).intersection(bi_value)) == 2: node_a = pair[0][0] * 9 + pair[0][1] node_c = pair[1][0] * 9 + pair[1][1] nodes = [node_a, node_b, node_c, node_d] boxes = {CELL_BOX[node] for node in nodes} if len(boxes) == 2: other_cells = set(CELLS_IN_ROW[pair[0][0]]).union(CELLS_IN_ROW[pair[1][0]]).union( CELLS_IN_COL[pair[0][1]]).union(CELLS_IN_COL[pair[1][1]]) other_candidates = ''.join(board[cell] for cell in other_cells) unique_value = None if other_candidates.count(bi_value[0]) == 4: unique_value = bi_value[0] elif other_candidates.count(bi_value[1]) == 4: unique_value = bi_value[1] if unique_value: other_candidates = set(board[node_b]).union(board[node_d]).difference(bi_value) c_chain = _get_c_chain(nodes, bi_value, other_candidates) to_eliminate = {(unique_value, node_b), (unique_value, node_d)} solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) if window: window.options_visible = window.options_visible.union(other_cells) kwargs["solver_tool"] = "uniqueness_test_6" kwargs["c_chain"] = c_chain kwargs["eliminate"] = to_eliminate test_6.clues += len(solver_status.naked_singles) test_6.options_removed += len(to_eliminate) return kwargs return None
def test_2(solver_status, board, window): """ Suppose both ceiling cells in the rectangle have exactly one extra candidate X. Then X can be eliminated from the cells seen by both of these cells. Rating: 100 """ def _check_rectangles(by_row): cells_by_x = CELLS_IN_ROW if by_row else CELLS_IN_COL cells_by_y = CELLS_IN_COL if by_row else CELLS_IN_ROW for floor_id_x in range(9): bi_values = _get_bi_values_dictionary(board, cells_by_x[floor_id_x], by_row) for bi_value, coordinates in bi_values.items(): if len(coordinates) == 2: floor_a = coordinates.pop() floor_b = coordinates.pop() x_ids = _get_xyz(1, board, bi_value, cells_by_y[floor_a[1]], cells_by_y[floor_b[1]]) for x_id in x_ids: ceiling_a = x_id * 9 + floor_a[1] if by_row else floor_a[1] * 9 + x_id ceiling_b = x_id * 9 + floor_b[1] if by_row else floor_b[1] * 9 + x_id boxes = {floor_a[2], floor_b[2], CELL_BOX[ceiling_a], CELL_BOX[ceiling_b]} if board[ceiling_a] == board[ceiling_b] and len(boxes) == 2: z_candidate = board[ceiling_a].replace(bi_value[0], '').replace(bi_value[1], '') to_eliminate = set() for cell in set(ALL_NBRS[ceiling_a]).intersection(ALL_NBRS[ceiling_b]): if z_candidate in board[cell]: to_eliminate.add((z_candidate, cell)) if to_eliminate: rows = (floor_a[0], x_id) if by_row else (floor_a[1], floor_b[1]) columns = (floor_a[1], floor_b[1]) if by_row else (floor_a[0], x_id) rectangle = _get_rectangle(sorted(rows), sorted(columns)) c_chain = _get_c_chain(rectangle, bi_value, {z_candidate, }) solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) if window: window.options_visible = window.options_visible.union(c_chain.keys()) kwargs["solver_tool"] = "uniqueness_test_2" kwargs["c_chain"] = c_chain kwargs["impacted_cells"] = {cell for _, cell in to_eliminate} kwargs["eliminate"] = to_eliminate test_2.clues += len(solver_status.naked_singles) test_2.options_removed += len(to_eliminate) return True return False set_remaining_candidates(board, solver_status) kwargs = {} if _check_rectangles(True) or _check_rectangles(False): return kwargs return None
def naked_single(solver_status, board, window): """ A naked single is the last remaining candidate in a cell. The Naked Single is categorized as a solving technique but you can hardly call it a technique. The only 'real work' is done when candidates in unsolved cells are not calculated yet: then the algorithm checks all possible candidates for each such cell to find the one with only one candidate. Otherwise, a naked single is that what remains after you have applied your solving techniques, by eliminating other candidates. Alternative terms are Forced Digit and Sole Candidate. Rating: 4 """ kwargs = {} if not (window or solver_status.pencilmarks): set_remaining_candidates(board, solver_status) naked_single.clues += len(solver_status.naked_singles) if solver_status.pencilmarks: if not solver_status.naked_singles: return None else: naked_singles_on_entry = len(solver_status.naked_singles) the_single = list(solver_status.naked_singles)[0] eliminate, impacted_cells = place_digit(the_single, board[the_single], board, solver_status, window) naked_single.options_removed += len(eliminate) naked_single.clues += len( solver_status.naked_singles) - naked_singles_on_entry + 1 kwargs["solver_tool"] = naked_single.__name__ if window: kwargs["cell_solved"] = the_single kwargs["eliminate"] = eliminate kwargs["house"] = get_impacted_houses( the_single, base_house=None, to_eliminate=impacted_cells) return kwargs else: for cell in range(81): if board[cell] == ".": cell_opts = get_cell_candidates(cell, board, solver_status) if len(cell_opts) == 1: solver_status.capture_baseline(board, window) board[cell] = cell_opts.pop() kwargs["solver_tool"] = naked_single.__name__ kwargs["cell_solved"] = cell solver_status.cells_solved.add(cell) naked_single.clues += 1 return kwargs return kwargs
def _naked_subset(solver_status, board, window, subset_size): """ Generic technique of finding naked subsets A Naked Subset is formed by N cells in a house with candidates for exactly N digits. N is the size of the subset, which must lie between 2 and the number of unsolved cells in the house minus 2. Since every Naked Subset is complemented by a Hidden Subset, the smallest of both sets will be no larger than 4 in a standard sized Sudoku. """ set_remaining_candidates(board, solver_status) subset_strategies = { 2: (naked_pair, 60), 3: (naked_triplet, 80), 4: (naked_quad, 120), } for house, subset_dict in get_subsets(board, subset_size): subset_cells = { cell for cells in subset_dict.values() for cell in cells } impacted_cells = get_impacted_cells(board, subset_cells) to_eliminate = { (candidate, cell) for cell in impacted_cells for candidate in set(board[cell]).intersection(subset_dict) } if to_eliminate: kwargs = {} if window: solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) subset_strategies[subset_size][0].clues += len( solver_status.naked_singles) subset_strategies[subset_size][0].options_removed += len( to_eliminate) kwargs["solver_tool"] = subset_strategies[subset_size][0].__name__ if window: window.options_visible = window.options_visible.union( subset_cells).union(impacted_cells) kwargs["chain_a"] = _get_chain(subset_cells, subset_dict) kwargs["eliminate"] = to_eliminate kwargs["house"] = impacted_cells.union(house) return kwargs return None
def test_5(solver_status, board, window): """ Suppose exactly two cells in the rectangle have exactly one extra candidate X, and both cells are located diagonally across each other in the rectangle. Then X can be eliminated from the cells seen by both of these cells. This would be called a Type 5 Unique Rectangle. Note that in this case the rectangle does not have a floor or ceiling. Rating: 100 """ set_remaining_candidates(board, solver_status) kwargs = {} bi_values = _get_bi_values_dictionary(board, range(81)) bi_values = {key: value for key, value in bi_values.items() if len(value) > 1} for bi_value in bi_values: for pair in combinations(bi_values[bi_value], 2): if pair[0][0] != pair[1][0] and pair[0][1] != pair[1][1] and pair[0][2] != pair[1][2]: node_b = pair[0][0] * 9 + pair[1][1] node_d = pair[1][0] * 9 + pair[0][1] if board[node_b] == board[node_d] and len(set(board[node_b]).difference(bi_value)) == 1: node_a = pair[0][0] * 9 + pair[0][1] node_c = pair[1][0] * 9 + pair[1][1] nodes = [node_a, node_b, node_c, node_d] boxes = {CELL_BOX[node] for node in nodes} if len(boxes) == 2: other_candidates = set(board[node_b]).difference(bi_value) c_chain = _get_c_chain(nodes, bi_value, other_candidates) z_value = other_candidates.pop() to_eliminate = {(z_value, cell) for cell in set(ALL_NBRS[node_b]).intersection(ALL_NBRS[node_d]) if z_value in board[cell]} if to_eliminate: solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) if window: window.options_visible = window.options_visible.union(c_chain.keys()) kwargs["solver_tool"] = "uniqueness_test_4" kwargs["c_chain"] = c_chain kwargs["impacted_cells"] = {cell for _, cell in to_eliminate} kwargs["eliminate"] = to_eliminate test_5.clues += len(solver_status.naked_singles) test_5.options_removed += len(to_eliminate) return kwargs return None
def skyscraper(solver_status, board, window): Rating: 130 """ TODO """ def _find_skyscraper(by_row, option): cells = CELLS_IN_ROW if by_row else CELLS_IN_COL for row_1 in range(8): cols_1 = set(col for col in range(9) if option in board[cells[row_1][col]] and len(board[cells[row_1][col]]) > 1) if len(cols_1) == 2: for row_2 in range(row_1+1, 9): cols_2 = set(col for col in range(9) if option in board[cells[row_2][col]] and len(board[cells[row_2][col]]) > 1) if len(cols_2) == 2 and len(cols_1.union(cols_2)) == 3: different_cols = cols_1.symmetric_difference(cols_2) cl_1_list = sorted(list(cols_1)) cl_2_list = sorted(list(cols_2)) corners = list() corners.append((row_1, cl_1_list[0]) if cl_1_list[0] not in different_cols else (row_1, cl_1_list[1])) corners.append((row_1, cl_1_list[0]) if cl_1_list[0] in different_cols else (row_1, cl_1_list[1])) corners.append((row_2, cl_2_list[0]) if cl_2_list[0] in different_cols else (row_2, cl_2_list[1])) corners.append((row_2, cl_2_list[0]) if cl_2_list[0] not in different_cols else (row_2, cl_2_list[1])) if by_row: corners_idx = [corners[i][0] * 9 + corners[i][1] for i in range(4)] else: corners_idx = [corners[i][1] * 9 + corners[i][0] for i in range(4)] impacted_cells = set(ALL_NBRS[corners_idx[1]]).intersection(ALL_NBRS[corners_idx[2]]) for corner in corners_idx: impacted_cells.discard(corner) clues = [cell for cell in impacted_cells if is_digit(cell, board, solver_status)] for clue_id in clues: impacted_cells.discard(clue_id) corners_idx.insert(0, option) to_eliminate = [(option, cell) for cell in impacted_cells if option in board[cell]] # TODO - check if not set if to_eliminate: solver_status.capture_baseline(board, window) house = set(cells[row_1]).union(set(cells[row_2])) if window: window.options_visible = window.options_visible.union(house).union(impacted_cells) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "skyscraper" kwargs["singles"] = solver_status.naked_singles kwargs["skyscraper"] = corners_idx kwargs["subset"] = [option] kwargs["eliminate"] = to_eliminate kwargs["house"] = house kwargs["impacted_cells"] = impacted_cells skyscraper.clues += len(solver_status.naked_singles) skyscraper.options_removed += len(to_eliminate) # print(f'\t{kwargs["solver_tool"]}') return True return False set_remaining_candidates(board, solver_status) kwargs = {} for opt in SUDOKU_VALUES_LIST: if _find_skyscraper(True, opt): return kwargs if _find_skyscraper(False, opt): return kwargs return kwargs
def test_3(solver_status, board, window): """ Suppose both ceiling cells have extra candidates. By treating these two cells as one node, find k - 1 other cells (as nodes) in the same house as these two cells so that the union of the candidates for these k cells has exactly k unique digits. Then the Naked Subset rule can be applied to eliminate these k digits from the other cells in the house. The algorithm is implemented for k = 2, 3 and 4 Rating: 100 """ def find_naked_subset(subset_size, ceiling_a, ceiling_b, bi_value, subset_candidates, by_row): search_area = {cell for cell in set(ALL_NBRS[ceiling_a]).intersection(ALL_NBRS[ceiling_b]) if len(board[cell]) > 1} possible_subset_nodes = {cell for cell in search_area if len(board[cell]) <= subset_size and set(board[cell]).intersection(subset_candidates) and not set(board[cell]).intersection(bi_value)} houses = [possible_subset_nodes.intersection(CELLS_IN_ROW[CELL_ROW[ceiling_a]] if by_row else CELLS_IN_COL[CELL_COL[ceiling_a]])] if CELL_BOX[ceiling_a] == CELL_BOX[ceiling_b]: houses.append(possible_subset_nodes.intersection(CELLS_IN_BOX[CELL_BOX[ceiling_a]])) for house in houses: for subset_nodes in combinations(house, subset_size-1): naked_subset = set("".join(board[cell] for cell in subset_nodes)).union(subset_candidates) if len(naked_subset) == subset_size: impacted_cells = search_area for cell in subset_nodes: impacted_cells = impacted_cells.intersection(ALL_NBRS[cell]) for cell in impacted_cells: for candidate in naked_subset.intersection(board[cell]): to_eliminate.add((candidate, cell)) if to_eliminate: return naked_subset, subset_nodes return None, None def _check_rectangles(by_row): cells_by_x = CELLS_IN_ROW if by_row else CELLS_IN_COL cells_by_y = CELLS_IN_COL if by_row else CELLS_IN_ROW for floor_id_x in range(9): bi_values = _get_bi_values_dictionary(board, cells_by_x[floor_id_x], by_row) for bi_value, coordinates in bi_values.items(): if len(coordinates) == 2: floor_a = coordinates.pop() floor_b = coordinates.pop() for n in (2, 3, 4): x_ids = _get_xyz(n, board, bi_value, cells_by_y[floor_a[1]], cells_by_y[floor_b[1]]) for x_id in x_ids: ceiling_a = x_id * 9 + floor_a[1] if by_row else floor_a[1] * 9 + x_id ceiling_b = x_id * 9 + floor_b[1] if by_row else floor_b[1] * 9 + x_id boxes = {floor_a[2], floor_b[2], CELL_BOX[ceiling_a], CELL_BOX[ceiling_b]} if len(boxes) == 2 and board[ceiling_a] != board[ceiling_b]: z_a = board[ceiling_a].replace(bi_value[0], '').replace(bi_value[1], '') z_b = board[ceiling_b].replace(bi_value[0], '').replace(bi_value[1], '') z_ab = set(z_a).union(z_b) naked_subset, subset_nodes = \ find_naked_subset(n, ceiling_a, ceiling_b, bi_value, z_ab, by_row) if to_eliminate: rows = (floor_a[0], x_id) if by_row else (floor_a[1], floor_b[1]) columns = (floor_a[1], floor_b[1]) if by_row else (floor_a[0], x_id) rectangle = _get_rectangle(sorted(rows), sorted(columns)) c_chain = _get_c_chain(rectangle, bi_value, naked_subset, subset_nodes) solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) if window: window.options_visible = window.options_visible.union(c_chain.keys()) kwargs["solver_tool"] = "uniqueness_test_3" kwargs["c_chain"] = c_chain kwargs["impacted_cells"] = {cell for _, cell in to_eliminate} kwargs["eliminate"] = to_eliminate test_3.clues += len(solver_status.naked_singles) test_3.options_removed += len(to_eliminate) return True return False set_remaining_candidates(board, solver_status) kwargs = {} to_eliminate = set() if _check_rectangles(True) or _check_rectangles(False): return kwargs return None
def xyz_wing(solver_status, board, window): """ Remove candidates (options) using XYZ Wing technique: For explanation of the technique see e.g.: - https://www.sudoku9981.com/sudoku-solving/xyz-wing.php Rating: 200-180 """ def _get_c_chain(root, wing_x, wing_y): z_value = set(board[wing_x]).intersection(set( board[wing_y])).intersection(set(board[root])).pop() x_value = set(board[wing_x]).difference({z_value}).pop() y_value = set(board[wing_y]).difference({z_value}).pop() return { root: {(x_value, 'lime'), (y_value, 'yellow'), (z_value, 'cyan')}, wing_x: {(x_value, 'yellow'), (z_value, 'cyan')}, wing_y: {(y_value, 'lime'), (z_value, 'cyan')} } def _find_xyz_wing(cell_id): xyz = set(board[cell_id]) bi_values = [ indx for indx in ALL_NBRS[cell_id] if len(board[indx]) == 2 ] for pair in combinations(bi_values, 2): xz = set(board[pair[0]]) yz = set(board[pair[1]]) if xz != yz and len(xyz.union(xz).union(yz)) == 3: z_value = xyz.intersection(xz).intersection(yz).pop() impacted_cells = set(ALL_NBRS[cell_id]).intersection( set(ALL_NBRS[pair[0]])).intersection(set( ALL_NBRS[pair[1]])) to_eliminate = [ (z_value, a_cell) for a_cell in impacted_cells if z_value in board[a_cell] and len(board[cell]) > 1 ] if to_eliminate: solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union( impacted_cells).union({cell_id, pair[0], pair[1]}) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "xyz_wing" kwargs["c_chain"] = _get_c_chain(cell_id, pair[0], pair[1]) kwargs["edges"] = [(cell_id, pair[0]), (cell_id, pair[1])] kwargs["eliminate"] = to_eliminate kwargs["impacted_cells"] = { a_cell for _, a_cell in to_eliminate } xyz_wing.options_removed += len(to_eliminate) xyz_wing.clues += len(solver_status.naked_singles) # print('\tXYZ-Wing') return True return False set_remaining_candidates(board, solver_status) kwargs = {} for cell in range(81): if len(board[cell]) == 3 and _find_xyz_wing(cell): return kwargs return kwargs
def _hidden_subset(solver_status, board, window, subset_size): """ Generic technique of finding hidden subsets A Hidden Subset is formed when N digits have only candidates in N cells in a house. A Hidden Subset is always complemented by a Naked Subset. Because Hidden Subsets are sometimes hard to find, players often prefer to look for Naked Subsets only, even when their size is greater. In a standard Sudoku, the maximum number of empty cells in a house is 9. There is no need to look for subsets larger than 4 cells, because the complementary subset will always be size 4 or smaller. """ set_remaining_candidates(board, solver_status) subset_strategies = { 2: (hidden_pair, 70), 3: (hidden_triplet, 100), 4: (hidden_quad, 150), } for cells in (CELLS_IN_ROW, CELLS_IN_COL, CELLS_IN_BOX): for house in cells: unsolved = {cell for cell in house if len(board[cell]) > 1} if len(unsolved) <= subset_size: continue candidates = set("".join(board[cell] for cell in unsolved)) if len(unsolved) != len(candidates): raise DeadEndException candidates_positions = { candidate: {cell for cell in unsolved if candidate in board[cell]} for candidate in candidates } for subset in itertools.combinations(candidates, subset_size): subset_nodes = { cell for candidate in subset for cell in candidates_positions[candidate] } if len(subset_nodes) == subset_size: to_eliminate = {(candidate, cell) for cell in subset_nodes for candidate in board[cell] if candidate not in subset} if to_eliminate: kwargs = {} if window: solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) subset_strategies[subset_size][0].clues += len( solver_status.naked_singles) subset_strategies[subset_size][ 0].options_removed += len(to_eliminate) kwargs["solver_tool"] = subset_strategies[subset_size][ 0].__name__ if window: window.options_visible = window.options_visible.union( unsolved) kwargs["chain_a"] = _get_chain( subset_nodes, candidates_positions) kwargs["eliminate"] = to_eliminate kwargs["house"] = house return kwargs return None
def w_wing(solver_status, board, window): """Remove candidates (options) using W-Wing technique http://hodoku.sourceforge.net/en/tech_wings.php#w Rating: 150 """ bi_value_cells = get_bi_value_cells(board) strong_links = get_strong_links(board) set_remaining_candidates(board, solver_status) for pair in bi_value_cells: if len(bi_value_cells[pair]) > 1: va, vb = pair w_constraint = None w_base = None for positions in combinations(bi_value_cells[pair], 2): for strong_link in strong_links[va]: if not set(positions).intersection(strong_link): sl_a = set(strong_link).intersection( ALL_NBRS[positions[0]]) sl_b = set(strong_link).intersection( ALL_NBRS[positions[1]]) if sl_a and sl_b and sl_a != sl_b: w_base = strong_link w_constraint = va break if w_constraint: break for strong_link in strong_links[vb]: if not set(positions).intersection(strong_link): sl_a = set(strong_link).intersection( ALL_NBRS[positions[0]]) sl_b = set(strong_link).intersection( ALL_NBRS[positions[1]]) if sl_a and sl_b and sl_a != sl_b: w_base = strong_link w_constraint = vb break if w_constraint: break else: continue assert w_constraint other_value = vb if w_constraint == va else va impacted_cells = set(ALL_NBRS[positions[0]]).intersection( ALL_NBRS[positions[1]]) impacted_cells = { cell for cell in impacted_cells if len(board[cell]) > 1 } to_eliminate = [(other_value, cell) for cell in impacted_cells if other_value in board[cell]] if to_eliminate: kwargs = {} solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union( impacted_cells).union({positions[0], positions[1] }).union(get_pair_house(w_base)) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "w_wing" kwargs["chain_a"] = _get_chain(board, positions[:2], other_value) kwargs["chain_d"] = _get_chain(board, w_base, other_value, w_constraint) kwargs["eliminate"] = to_eliminate kwargs["impacted_cells"] = { a_cell for _, a_cell in to_eliminate } w_wing.options_removed += len(to_eliminate) w_wing.clues += len(solver_status.naked_singles) # print('\tw_wing') return kwargs return {}
def _fish(solver_status, board, window, n): """ Generic 'fish' technique """ fish_strategies = { 2: (x_wing, 100), 3: (swordfish, 140), 4: (jellyfish, 470), 5: (squirmbag, 470), } def _get_chain(candidate, cells, primary_ids): nodes = { cell for idx in primary_ids for cell in cells[idx] if candidate in board[cell] } return {node: {(candidate, 'cyan')} for node in nodes} def _find_fish(by_row): for value in SUDOKU_VALUES_LIST: lines = get_2_upto_n_candidates(board, value, n, by_row) if len(lines) >= n: for x_ids in combinations((id_x for id_x in lines), n): y_ids = set(id_y for id_x in x_ids for id_y in lines[id_x]) if len(y_ids) == n: cells = CELLS_IN_COL if by_row else CELLS_IN_ROW impacted_cells = { cell for id_y in y_ids for cell in cells[id_y] } cells = CELLS_IN_ROW if by_row else CELLS_IN_COL houses = { cell for id_x in x_ids for cell in cells[id_x] } impacted_cells = impacted_cells.difference(houses) to_eliminate = {(value, cell) for cell in impacted_cells if value in board[cell]} if to_eliminate: if window: solver_status.capture_baseline(board, window) kwargs["solver_tool"] = fish_strategies[n][ 0].__name__ eliminate_options(solver_status, board, to_eliminate, window) fish_strategies[n][0].clues += len( solver_status.naked_singles) fish_strategies[n][0].options_removed += len( to_eliminate) if window: impacted = _get_impacted_lines( to_eliminate, by_row, board) window.options_visible = window.options_visible.union( houses).union(impacted_cells) kwargs["chain_a"] = _get_chain( value, cells, x_ids) kwargs["eliminate"] = to_eliminate kwargs["house"] = impacted.union(houses) return True return False set_remaining_candidates(board, solver_status) kwargs = {} if _find_fish(True) or _find_fish(False): return kwargs return None
def franken_x_wing(solver_status, board, window): """ TODO """ by_row_boxes = { 0: (3, 6), 1: (4, 7), 2: (5, 8), 3: (0, 6), 4: (1, 7), 5: (2, 8), 6: (0, 3), 7: (1, 4), 8: (2, 5), } by_col_boxes = { 0: (1, 2), 1: (0, 2), 2: (0, 1), 3: (4, 5), 4: (3, 5), 5: (3, 4), 6: (7, 8), 7: (6, 8), 8: (6, 7), } def _find_franken_x_wing(by_row, option): cells = CELLS_IN_ROW if by_row else CELLS_IN_COL for row in range(9): cols_1 = [ col for col in range(9) if option in board[cells[row][col]] and len(board[cells[row][col]]) > 1 ] if len(cols_1) == 2: corner_1 = cells[row][cols_1[0]] corner_2 = cells[row][cols_1[1]] if CELL_BOX[corner_1] == CELL_BOX[corner_2]: other_boxes = by_row_boxes[ CELL_BOX[corner_1]] if by_row else by_col_boxes[ CELL_BOX[corner_1]] for box in other_boxes: if by_row: cols_2 = set(CELL_COL[cell] for cell in CELLS_IN_BOX[box] if option in board[cell] and len(board[cell]) > 1) else: cols_2 = set(CELL_ROW[cell] for cell in CELLS_IN_BOX[box] if option in board[cell] and len(board[cell]) > 1) if set(cols_1) == cols_2: if by_row: other_cells = set( CELLS_IN_COL[cols_1[0]]).union( set(CELLS_IN_COL[cols_1[1]])) else: other_cells = set( CELLS_IN_ROW[cols_1[0]]).union( set(CELLS_IN_ROW[cols_1[1]])) other_cells = other_cells.intersection( set(CELLS_IN_BOX[CELL_BOX[corner_1]])) other_cells.discard(corner_1) other_cells.discard(corner_2) house = set(cells[row]).union( set(CELLS_IN_BOX[box])) corners = [option, corner_1, corner_2] corners.extend(cell for cell in CELLS_IN_BOX[box] if option in board[cell]) to_eliminate = [(option, cell) for cell in other_cells if option in board[cell]] if to_eliminate: solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union( house).union(other_cells) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "franken_x_wing" kwargs["singles"] = solver_status.naked_singles kwargs["finned_x_wing"] = corners kwargs["subset"] = [option] kwargs["eliminate"] = to_eliminate kwargs["house"] = house kwargs["impacted_cells"] = other_cells return True return False set_remaining_candidates(board, solver_status) kwargs = {} for opt in SUDOKU_VALUES_LIST: if _find_franken_x_wing(True, opt): return kwargs if _find_franken_x_wing(False, opt): return kwargs return kwargs
def locked_candidates(solver_status, board, window): """ A solving technique that uses the intersections between lines and boxes. Aliases include: Intersection Removal, Line-Box Interaction. The terms Pointing and Claiming/Box-Line Reduction are often used to distinguish the 2 types. This is a basic solving technique. When all candidates for a digit in a house are located inside the intersection with another house, we can eliminate the remaining candidates from the second house outside the intersection. """ def _paint_locked_candidates(house, locked_candidate): return { cell: { (locked_candidate, "cyan"), } for cell in house if locked_candidate in board[cell] } def _type_1(): """ Type 1 (Pointing) All the candidates for digit X in a box are confined to a single line (row or column). The surplus candidates are eliminated from the part of the line that does not intersect with this box. Rating: 50 """ for house in CELLS_IN_BOX: candidates = SUDOKU_VALUES_SET - { board[cell] for cell in house if len(board[cell]) == 1 } unsolved = {cell for cell in house if len(board[cell]) > 1} for possibility in candidates: in_rows = set(CELL_ROW[cell] for cell in unsolved if possibility in board[cell]) in_cols = set(CELL_COL[cell] for cell in unsolved if possibility in board[cell]) impacted_cells = None if len(in_rows) == 1: impacted_cells = set( CELLS_IN_ROW[in_rows.pop()]).difference(house) elif len(in_cols) == 1: impacted_cells = set( CELLS_IN_COL[in_cols.pop()]).difference(house) if impacted_cells: to_eliminate = {(possibility, cell) for cell in impacted_cells if possibility in board[cell]} if to_eliminate: if window: solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) locked_candidates.clues += len( solver_status.naked_singles) locked_candidates.options_removed += len(to_eliminate) kwargs["solver_tool"] = "locked_candidates_type_1" if window: window.options_visible = window.options_visible.union( house).union(impacted_cells) kwargs["house"] = impacted_cells.union(house) kwargs["eliminate"] = to_eliminate kwargs["chain_a"] = _paint_locked_candidates( house, possibility) return True return False def _type_2(): """ Type 2 (Claiming or Box-Line Reduction) All the candidates for digit X in a line are confined to a single box. The surplus candidates are eliminated from the part of the box that does not intersect with this line. Rating: 50 - 60 """ for cells in (CELLS_IN_ROW, CELLS_IN_COL): for house in cells: candidates = SUDOKU_VALUES_SET - { board[cell] for cell in house if len(board[cell]) == 1 } unsolved = {cell for cell in house if len(board[cell]) > 1} for possibility in candidates: boxes = { CELL_BOX[cell] for cell in unsolved if possibility in board[cell] } if len(boxes) == 1: impacted_cells = set( CELLS_IN_BOX[boxes.pop()]).difference(house) to_eliminate = {(possibility, cell) for cell in impacted_cells if possibility in board[cell]} if to_eliminate: if window: solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) locked_candidates.clues += len( solver_status.naked_singles) locked_candidates.options_removed += len( to_eliminate) kwargs["solver_tool"] = "locked_candidates_type_2" if window: window.options_visible = window.options_visible.union( house).union(impacted_cells) kwargs["house"] = impacted_cells.union(house) kwargs["eliminate"] = to_eliminate kwargs["chain_a"] = _paint_locked_candidates( house, possibility) return True return False set_remaining_candidates(board, solver_status) kwargs = {} if _type_1() or _type_2(): return kwargs return None
eliminate_options(solver_status, board, to_eliminate, window) fish_strategies[n][0].clues += len( solver_status.naked_singles) fish_strategies[n][ 0].options_removed += len(to_eliminate) kwargs["solver_tool"] = fish_strategies[n][ 0].__name__ if window: kwargs["chain_a"] = _get_fish_nodes( candidate, cells, x_ids) kwargs["eliminate"] = to_eliminate return True return False set_remaining_candidates(board, solver_status) kwargs = {} if _find_finned_fish(True) or _find_finned_fish(False): return kwargs return None def _sashimi_fish(solver_status, board, window, n): """ Generic 'sashimi fish' technique """ fish_strategies = { 2: (sashimi_x_wing, 150), 3: (sashimi_swordfish, 240), 4: (sashimi_jellyfish, 280), 5: (sashimi_squirmbag, 470), }
def empty_rectangle(solver_status, board, window): """ The relatively good description of Empty Rectangle strategy is available at Sudoku Coach page (http://www.taupierbw.be/SudokuCoach/SC_EmptyRectangle.shtml) - although it is not complete Rating: 120 - 140 """ by_row_boxes = [[[3, 6], [4, 7], [5, 8]], [[0, 6], [1, 7], [2, 8]], [[0, 3], [1, 4], [2, 5]]] by_col_boxes = [[[1, 2], [4, 5], [7, 8]], [[0, 2], [3, 5], [6, 8]], [[0, 1], [3, 4], [6, 7]]] def _find_empty_rectangle(idx, by_row): cells_by_x = CELLS_IN_ROW if by_row else CELLS_IN_COL cells_by_y = CELLS_IN_COL if by_row else CELLS_IN_ROW cells = cells_by_x[idx] opts = ''.join(board[cell] for cell in cells if len(board[cell]) > 1) for val in SUDOKU_VALUES_LIST: if opts.count(val) == 2: idy = [j for j in range(9) if val in board[cells[j]]] if CELL_BOX[idy[0]] != CELL_BOX[idy[1]]: for i in range(2): for j in range(2): box = by_row_boxes[idx//3][idy[i]//3][j] if by_row else by_col_boxes[idx//3][idy[i]//3][j] central_line = (box // 3) * 3 + 1 if by_row else (box % 3) * 3 + 1 box_cells = set(CELLS_IN_BOX[box]) central_line_cells = set(cells_by_x[central_line]).intersection(box_cells) cross_cells = box_cells.intersection(central_line_cells.union(set(cells_by_y[idy[i]]))) rect_corners = box_cells.difference(cross_cells) corners_values = ''.join(board[cell] for cell in rect_corners) if corners_values.count(val) == 0: hole_cells = list(central_line_cells.difference(set(cells_by_y[idy[i]]))) if val in board[hole_cells[0]] or val in board[hole_cells[1]]: impacted_cell = cells_by_y[idy[(i + 1) % 2]][central_line] if val in board[impacted_cell]: to_eliminate = [(val, impacted_cell)] if to_eliminate: corners = set(cell for cell in cells_by_x[idx] if val in board[cell]) if val in board[hole_cells[0]]: corners.add(hole_cells[0]) if val in board[hole_cells[1]]: corners.add(hole_cells[1]) corners = list(corners) corners.insert(0, val) house = set(cells).union(cross_cells) solver_status.capture_baseline(board, window) solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union(house) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "empty_rectangle" kwargs["house"] = house kwargs["impacted_cells"] = (impacted_cell,) kwargs["eliminate"] = [(val, impacted_cell)] kwargs["nodes"] = corners empty_rectangle.clues += len(solver_status.naked_singles) empty_rectangle.options_removed += len(to_eliminate) return True return False set_remaining_candidates(board, solver_status) kwargs = {} for indx in range(9): if _find_empty_rectangle(indx, True): return kwargs if _find_empty_rectangle(indx, False): return kwargs return kwargs
def finned_x_wing(solver_status, board, window): """ TODO """ def _find_finned_x_wing(by_row, option): cells = CELLS_IN_ROW if by_row else CELLS_IN_COL for row_1 in range(9): r1_cols = set(col for col in range(9) if option in board[cells[row_1][col]] and len(board[cells[row_1][col]]) > 1) if len(r1_cols) == 2: for row_2 in range(9): if row_2 != row_1: r2_cols = set(col for col in range(9) if option in board[cells[row_2][col]] and len(board[cells[row_2][col]]) > 1) if r2_cols.issuperset(r1_cols): fin = r2_cols.difference(r1_cols) other_cells = set() house = set() corners = list() if len(fin) == 1: col_1 = r1_cols.pop() col_2 = r1_cols.pop() col_f = fin.pop() if by_row: box_1 = CELL_BOX[row_2 * 9 + col_1] box_2 = CELL_BOX[row_2 * 9 + col_2] box_f = CELL_BOX[row_2 * 9 + col_f] corners = [ option, row_1 * 9 + col_1, row_1 * 9 + col_2, row_2 * 9 + col_1, row_2 * 9 + col_f, row_2 * 9 + col_2 ] house = set(CELLS_IN_ROW[row_1]).union( set(CELLS_IN_ROW[row_2])) if box_1 == box_f: other_cells = set( CELLS_IN_BOX[box_1]).intersection( set(CELLS_IN_COL[col_1])) other_cells.discard(row_1 * 9 + col_1) other_cells.discard(row_2 * 9 + col_1) elif box_2 == box_f: other_cells = set( CELLS_IN_BOX[box_2]).intersection( set(CELLS_IN_COL[col_2])) other_cells.discard(row_1 * 9 + col_2) other_cells.discard(row_2 * 9 + col_2) else: box_1 = CELL_BOX[col_1 * 9 + row_2] box_2 = CELL_BOX[col_2 * 9 + row_2] box_f = CELL_BOX[col_f * 9 + row_2] corners = [ option, col_1 * 9 + row_1, col_2 * 9 + row_1, col_1 * 9 + row_2, col_f * 9 + row_2, col_2 * 9 + row_2 ] house = set(CELLS_IN_COL[row_1]).union( set(CELLS_IN_COL[row_2])) if box_f == box_1: other_cells = set( CELLS_IN_BOX[box_1]).intersection( set(CELLS_IN_ROW[col_1])) other_cells.discard(col_1 * 9 + row_1) other_cells.discard(col_1 * 9 + row_2) elif box_f == box_2: other_cells = set( CELLS_IN_BOX[box_2]).intersection( set(CELLS_IN_ROW[col_2])) other_cells.discard(col_2 * 9 + row_1) other_cells.discard(col_2 * 9 + row_2) if other_cells: to_eliminate = [(option, cell) for cell in other_cells if option in board[cell]] if to_eliminate: solver_status.capture_baseline( board, window) if window: window.options_visible = window.options_visible.union( house).union(other_cells) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "finned_x_wings" kwargs[ "singles"] = solver_status.naked_singles kwargs["finned_x_wing"] = corners kwargs["subset"] = [option] kwargs["eliminate"] = to_eliminate kwargs["house"] = house kwargs["impacted_cells"] = other_cells finned_x_wing.options_removed += len( to_eliminate) finned_x_wing.clues += len( solver_status.naked_singles) # print('\tfinned X-wing') return True return False set_remaining_candidates(board, solver_status) kwargs = {} for opt in SUDOKU_VALUES_LIST: if _find_finned_x_wing(True, opt): return kwargs if _find_finned_x_wing(False, opt): return kwargs return kwargs
def two_string_kite(solver_status, board, window): """ Two crossing strong links, weakly connected in a box """ def _get_strings(digit, lines): strings = set() for line in lines: cells = tuple(cell for cell in line if digit in board[cell]) in_boxes = tuple(CELL_BOX[cell] for cell in cells) if len(cells) in (2, 3) and len(set(in_boxes)) == 2: strings.add(ConjugateCells(cells, in_boxes)) return strings set_remaining_candidates(board, solver_status) kwargs = {} for candidate in SUDOKU_VALUES_LIST: x_strings = _get_strings(candidate, CELLS_IN_ROW) y_strings = _get_strings(candidate, CELLS_IN_COL) for x_string in x_strings: for y_string in y_strings: nodes = {cell for cell in x_string.cells }.union(cell for cell in y_string.cells) boxes = {box for box in x_string.boxes }.union(box for box in y_string.boxes) if len(nodes) == (len(x_string.cells) + len(y_string.cells)) and len(boxes) == 3: end_box_x = boxes.difference(y_string.boxes).pop() end_box_y = boxes.difference(x_string.boxes).pop() if x_string.boxes.count( end_box_x) == 1 and y_string.boxes.count( end_box_y) == 1: end_x = { cell for i, cell in enumerate(x_string.cells) if x_string.boxes[i] == end_box_x } end_y = { cell for i, cell in enumerate(y_string.cells) if y_string.boxes[i] == end_box_y } assert len(end_x) == 1 assert len(end_y) == 1 end_x = end_x.pop() end_y = end_y.pop() impacted = set( CELLS_IN_COL[CELL_COL[end_x]]).intersection( CELLS_IN_ROW[CELL_ROW[end_y]]) assert len(impacted) == 1 impacted = impacted.pop() if candidate in board[impacted]: to_eliminate = { (candidate, impacted), } if window: solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) two_string_kite.clues += len( solver_status.naked_singles) two_string_kite.options_removed += 1 kwargs["solver_tool"] = two_string_kite.__name__ if window: houses = set( CELLS_IN_ROW[CELL_ROW[end_x]]).union( CELLS_IN_COL[CELL_COL[end_y]]).union({ impacted, }) window.options_visible = window.options_visible.union( houses) kwargs["chain_a"] = { cell: { (candidate, 'cyan'), } for cell in nodes } kwargs["eliminate"] = to_eliminate kwargs["house"] = houses return kwargs return None
def empty_rectangle(solver_status, board, window): """ TODO """ def get_er_boxes(candidate): er_boxes = set() for box_id in range(9): with_candidate = set(cell for cell in CELLS_IN_BOX[box_id] if candidate in board[cell]) if len(with_candidate) == 4: in_rows = [CELL_ROW[cell] for cell in with_candidate] in_columns = [CELL_COL[cell] for cell in with_candidate] if len(set(in_rows)) == 3 and len(set(in_columns)) == 3: for row in set(in_rows): if in_rows.count(row) == 2: row_pair = { cell for cell in with_candidate if CELL_ROW[cell] == row } for column in set(in_columns): if in_columns.count(column) == 2: col_pair = { cell for cell in with_candidate if CELL_COL[cell] == column } if len(row_pair.union(col_pair)) == 4: er_boxes.add( ErBox(tuple(with_candidate), box_id, row, column)) return er_boxes def get_conjugate_pairs(candidate, lines): conjugate_pairs = set() for line in lines: in_cells = set(cell for cell in line if candidate in board[cell]) if len(in_cells) == 2: cell_a = in_cells.pop() cell_b = in_cells.pop() if CELL_BOX[cell_a] != CELL_BOX[cell_b]: conjugate_pairs.add(ConjugatePair(cell_a, cell_b)) return conjugate_pairs def basic_empty_rectangle(): """ canonical form of empty rectangle pattern """ for candidate in SUDOKU_VALUES_LIST: er_boxes = get_er_boxes(candidate) # if er_boxes: # print(f'\tDupa') # return BasicErPattern(candidate, set(er_boxes.pop().in_cells), 0) conjugate_pairs = get_conjugate_pairs(candidate, CELLS_IN_ROW) for conjugate_pair in conjugate_pairs: for er_box in er_boxes: if CELL_COL[conjugate_pair.cell_a] == er_box.column\ and CELL_BOX[conjugate_pair.cell_a] != er_box.box: impacted = er_box.row * 9 + CELL_COL[ conjugate_pair.cell_b] if candidate in board[impacted]: return BasicErPattern( candidate, set(er_box.in_cells).union({ conjugate_pair.cell_a, conjugate_pair.cell_b }), impacted) if CELL_COL[conjugate_pair.cell_b] == er_box.column\ and CELL_BOX[conjugate_pair.cell_b] != er_box.box: impacted = er_box.row * 9 + CELL_COL[ conjugate_pair.cell_a] if candidate in board[impacted]: return BasicErPattern( candidate, set(er_box.in_cells).union({ conjugate_pair.cell_a, conjugate_pair.cell_b }), impacted) conjugate_pairs = get_conjugate_pairs(candidate, CELLS_IN_COL) for conjugate_pair in conjugate_pairs: for er_box in er_boxes: if CELL_ROW[conjugate_pair.cell_a] == er_box.row\ and CELL_BOX[conjugate_pair.cell_a] != er_box.box: impacted = CELL_ROW[ conjugate_pair.cell_b] * 9 + er_box.column if candidate in board[impacted]: return BasicErPattern( candidate, set(er_box.in_cells).union({ conjugate_pair.cell_a, conjugate_pair.cell_b }), impacted) if CELL_ROW[conjugate_pair.cell_b] == er_box.row\ and CELL_BOX[conjugate_pair.cell_b] != er_box.box: impacted = CELL_ROW[ conjugate_pair.cell_a] * 9 + er_box.column if candidate in board[impacted]: return BasicErPattern( candidate, set(er_box.in_cells).union({ conjugate_pair.cell_a, conjugate_pair.cell_b }), impacted) return None set_remaining_candidates(board, solver_status) basic_er_pattern = basic_empty_rectangle() if basic_er_pattern: kwargs = {} to_eliminate = { (basic_er_pattern.candidate, basic_er_pattern.impacted), } if window: solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) empty_rectangle.clues += len(solver_status.naked_singles) empty_rectangle.options_removed += 1 kwargs["solver_tool"] = empty_rectangle.__name__ if window: kwargs["chain_a"] = { cell: { (basic_er_pattern.candidate, 'cyan'), } for cell in basic_er_pattern.pattern } kwargs["eliminate"] = to_eliminate print(f'\t{kwargs["solver_tool"]}') return kwargs return None
def naked_xy_chain(solver_status, board, window): """ Remove candidates (options) using XY Wing technique: For explanation of the technique see e.g.: - http://www.sudokusnake.com/nakedxychains.php The strategy is assessed as 'Hard', 'Unfair', or 'Diabolical'. Ranking of the method (called XY-Chain) varies widely 260 and 900 Implementation comments: Building a graph and identifying potential chains (paths) was straightforward. A slightly tricky part was to add checking for possibility of bidirectional traversing between end nodes of the potential paths """ def _build_bi_value_cells_graph(): bi_value_cells = set(cell for cell in range(81) if len(board[cell]) == 2) graph = nx.Graph() for cell in bi_value_cells: neighbours = set(ALL_NBRS[cell]).intersection(bi_value_cells) graph.add_edges_from([(cell, other_cell, { 'candidates': set(board[cell]).intersection(set(board[other_cell])) }) for other_cell in neighbours if len( set(board[cell]).intersection(set(board[other_cell]))) == 1]) return graph set_remaining_candidates(board, solver_status) graph = _build_bi_value_cells_graph() components = list(nx.connected_components(graph)) unresolved = [cell for cell in range(81) if len(board[cell]) > 2] kwargs = {} for cell in unresolved: for component in components: nodes = component.intersection(set(ALL_NBRS[cell])) candidates = ''.join(board[node] for node in nodes) for candidate in board[cell]: if candidates.count(candidate) == 2: ends = [node for node in nodes if candidate in board[node]] path = nx.algorithms.shortest_paths.generic.shortest_path( graph, ends[0], ends[1]) if _check_bidirectional_traversing(candidate, path, board): impacted_cells = { cell for cell in set(ALL_NBRS[path[0]]).intersection( set(ALL_NBRS[path[-1]])) if len(board[cell]) > 1 } to_eliminate = [(candidate, cell) for cell in impacted_cells if candidate in board[cell]] edges = [(path[n], path[n + 1]) for n in range(len(path) - 1)] solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union( _get_graph_houses(edges)).union({cell}) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "naked_xy_chain" kwargs["impacted_cells"] = impacted_cells kwargs["eliminate"] = to_eliminate kwargs["c_chain"] = _color_naked_xy_chain( graph, path, candidate) kwargs["edges"] = edges naked_xy_chain.options_removed += len(to_eliminate) naked_xy_chain.clues += len( solver_status.naked_singles) return kwargs return kwargs
def sue_de_coq(solver_status, board, window): """ TODO """ def _find_sue_de_coq_type_1(box, by_rows): for cell_1 in CELLS_IN_BOX[box]: if len(board[cell_1]) == 2: if by_rows: indexes = [row for row in range((box // 3) * 3, (box // 3) * 3 + 3) if row != CELL_ROW[cell_1]] else: indexes = [col for col in range((box % 3) * 3, (box % 3) * 3 + 3) if col != CELL_COL[cell_1]] for indx in indexes: cells_b = set(CELLS_IN_BOX[box]) cells_1 = set(CELLS_IN_ROW[indx]) if by_rows else set(CELLS_IN_COL[indx]) cells_2 = [cell for cell in cells_1.difference(cells_b) if len(board[cell]) > 1] cells_3 = [cell for cell in cells_1.intersection(cells_b) if len(board[cell]) > 1] cells_4 = [cell for cell in cells_b.difference(cells_1) if len(board[cell]) > 1] if len(cells_3) > 1: for cell_2 in cells_2: if len(board[cell_2]) == 2 and not set(board[cell_1]).intersection(set(board[cell_2])): options_12 = set(board[cell_1]).union(set(board[cell_2])) for pair in combinations(cells_3, 2): if options_12 == set(board[pair[0]]).union(board[pair[1]]): to_eliminate = [] for opt in board[cell_1]: for cell in cells_4: if cell != cell_1 and opt in board[cell]: to_eliminate.append((opt, cell)) for opt in board[cell_2]: for cell in cells_1: if cell != cell_2 and cell != pair[0] and cell != pair[1] \ and opt in board[cell]: to_eliminate.append((opt, cell)) if to_eliminate: solver_status.capture_baseline(board, window) house = cells_b.union(cells_1) pattern = {cell_1, cell_2, pair[0], pair[1]} impacted_cells = set(cells_2).union(set(cells_3)).union(set(cells_4)) impacted_cells.difference(pattern) if window: window.options_visible = window.options_visible.union(house).union( house) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "sue_de_coq" kwargs["singles"] = solver_status.naked_singles kwargs["sue_de_coq"] = pattern kwargs["eliminate"] = to_eliminate kwargs["house"] = house kwargs["impacted_cells"] = impacted_cells kwargs["subset"] = [to_eliminate[0][0]] return True return False def _find_sue_de_coq_type_2(box, by_rows): for cell_1 in CELLS_IN_BOX[box]: if len(board[cell_1]) == 2: if by_rows: indexes = [row for row in range((box // 3) * 3, (box // 3) * 3 + 3) if row != CELL_ROW[cell_1]] else: indexes = [col for col in range((box % 3) * 3, (box % 3) * 3 + 3) if col != CELL_COL[cell_1]] for indx in indexes: cells_b = set(CELLS_IN_BOX[box]) cells_1 = set(CELLS_IN_ROW[indx]) if by_rows else set(CELLS_IN_COL[indx]) cells_2 = [cell for cell in cells_1.difference(cells_b) if len(board[cell]) > 1] cells_3 = [cell for cell in cells_1.intersection(cells_b) if len(board[cell]) > 1] cells_4 = [cell for cell in cells_b.difference(cells_1) if len(board[cell]) > 1] if len(cells_3) == 3: for cell_2 in cells_2: if len(board[cell_2]) == 2 and not set(board[cell_1]).intersection(set(board[cell_2])): options_12 = set(board[cell_1]).union(set(board[cell_2])) options_3 = set(board[cells_3[0]]).union(set(board[cells_3[1]])).union( set(board[cells_3[2]])) if options_3.issuperset(options_12) and len(options_3.difference(options_12)) == 1: to_eliminate = [] for opt in board[cell_1]: for cell in cells_4: if cell != cell_1 and opt in board[cell]: to_eliminate.append((opt, cell)) for opt in board[cell_2]: for cell in cells_2: if cell != cell_2 and opt in board[cell]: to_eliminate.append((opt, cell)) opt = options_3.difference(options_12).pop() for cell in set(cells_2).union(set(cells_4)): if opt in board[cell]: to_eliminate.append((opt, cell)) if to_eliminate: solver_status.capture_baseline(board, window) house = cells_b.union(cells_1) pattern = {cell_1, cell_2}.union(cells_3) impacted_cells = set(cells_2).union(set(cells_3)).union(set(cells_4)) impacted_cells.difference(pattern) if window: window.options_visible = window.options_visible.union(house).union( house) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "sue_de_coq" kwargs["singles"] = solver_status.naked_singles kwargs["sue_de_coq"] = pattern kwargs["eliminate"] = to_eliminate kwargs["house"] = house kwargs["impacted_cells"] = impacted_cells kwargs["subset"] = [to_eliminate[0][0]] return True return False set_remaining_candidates(board, solver_status) kwargs = {} for sqr in range(9): if _find_sue_de_coq_type_1(sqr, True): return kwargs if _find_sue_de_coq_type_2(sqr, True): return kwargs if _find_sue_de_coq_type_1(sqr, False): return kwargs if _find_sue_de_coq_type_2(sqr, False): return kwargs return kwargs
def finned_mutant_x_wing(solver_status, board, window): """ TODO """ def _find_finned_rccb_mutant_x_wing(box_id): pairs_dict = get_house_pairs(CELLS_IN_BOX[box_id], board) for pair, cells in pairs_dict.items(): if len(cells) == 2: cells_pos = [(CELL_ROW[cells[0]], CELL_COL[cells[1]]), (CELL_ROW[cells[1]], CELL_COL[cells[0]])] values = (pair[0], pair[1]) for value in values: for row, col in cells_pos: col_2 = [ CELL_COL[cell] for cell in CELLS_IN_ROW[row] if value in board[cell] and CELL_BOX[cell] != box_id ] row_2 = [ CELL_ROW[cell] for cell in CELLS_IN_COL[col] if value in board[cell] and CELL_BOX[cell] != box_id ] if len(col_2) == 1 and len(row_2) == 1: impacted_cell = set( CELLS_IN_COL[col_2[0]]).intersection( set(CELLS_IN_ROW[row_2[0]])) cell = impacted_cell.pop() if value in board[cell]: house = set(CELLS_IN_ROW[row]).union( set(CELLS_IN_COL[col])) impacted_cell = {cell} to_eliminate = [ (value, cell), ] corners = [ value, cells[0], cells[1], row * 9 + col_2[0], row_2[0] * 9 + col ] solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union( house).union(impacted_cell) eliminate_options(solver_status, board, to_eliminate, window) kwargs[ "solver_tool"] = "finned_rccb_mutant_x_wing" kwargs["eliminate"] = to_eliminate kwargs["house"] = house kwargs["impacted_cells"] = impacted_cell kwargs["finned_x_wing"] = corners finned_mutant_x_wing.options_removed += len( to_eliminate) finned_mutant_x_wing.clues += len( solver_status.naked_singles) return True return False def _find_finned_cbrc_mutant_x_wing(by_column, indx): house_1 = set(CELLS_IN_COL[indx]) if by_column else set( CELLS_IN_ROW[indx]) pairs_dict = get_house_pairs(house_1, board) for pair, cells in pairs_dict.items(): if len(cells) == 2 and CELL_BOX[cells[0]] != CELL_BOX[cells[1]]: cells_pos = [(CELL_ROW[cells[0]], CELL_COL[cells[0]]), (CELL_ROW[cells[1]], CELL_COL[cells[1]])] values = (pair[0], pair[1]) for value in values: for row, col in cells_pos: house_2 = set(CELLS_IN_ROW[row]) if by_column else set( CELLS_IN_COL[col]) boxes = [ box for box in range(9) if set(CELLS_IN_BOX[box]).intersection(house_2) and not set(CELLS_IN_BOX[box]).intersection(house_1) ] for box in boxes: fins = [ cell for cell in set( CELLS_IN_BOX[box]).difference(house_2) if value in board[cell] and len(board[cell]) > 1 ] impacted_cell = None if by_column: col_2 = CELL_COL[fins[0]] if fins else None for fin in fins[1:]: col_2 = col_2 if CELL_COL[ fin] == col_2 else None if col_2 is not None: row_2 = cells_pos[0][0] if row == cells_pos[ 1][0] else cells_pos[1][0] impacted_cell = row_2 * 9 + col_2 else: row_2 = CELL_ROW[fins[0]] if fins else None for fin in fins[1:]: row_2 = row_2 if CELL_ROW[ fin] == row_2 else None if row_2 is not None: col_2 = cells_pos[0][1] if col == cells_pos[ 1][1] else cells_pos[1][1] impacted_cell = row_2 * 9 + col_2 if impacted_cell is not None and value in board[ impacted_cell]: house = house_1.union(set(CELLS_IN_BOX[box])) to_eliminate = [ (value, impacted_cell), ] corners = [cell for cell in cells] corners.extend(fins) corners.insert(0, value) solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union( house).union({impacted_cell}) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = \ "finned_cbrc_mutant_x_wing" if by_column else "finned_rbcc_mutant_x_wing" kwargs["eliminate"] = to_eliminate kwargs["house"] = house kwargs["impacted_cells"] = {impacted_cell} kwargs["finned_x_wing"] = corners finned_mutant_x_wing.options_removed += len( to_eliminate) finned_mutant_x_wing.clues += len( solver_status.naked_singles) # if len(fins) > 1: # print('\nBingo!') return True return False set_remaining_candidates(board, solver_status) kwargs = {} for i in range(9): if _find_finned_rccb_mutant_x_wing(i): # print('\nFinned RCCB Mutant X-Wing') return kwargs if _find_finned_cbrc_mutant_x_wing(True, i): # print('\nFinned CBRC Mutant X-Wing') return kwargs if _find_finned_cbrc_mutant_x_wing(False, i): # print('\nFinned RBCC Mutant X-Wing') return kwargs return kwargs