def _find_sashimi_fish(by_row): for candidate in SUDOKU_VALUES_LIST: lines = get_2_upto_n_candidates(board, candidate, n + 1, by_row) if len(lines) >= n: for x_ids in combinations(lines, n): cells = CELLS_IN_ROW if by_row else CELLS_IN_COL houses = {cell for x_id in x_ids for cell in cells[x_id]} all_y_ids = set(idy for idx in x_ids for idy in lines[idx]) if len(all_y_ids) in (n, n + 1): if fins := _get_fins(lines, x_ids): impacted_cells = set() for fin_x in fins: fin_ys = list(fins[fin_x]) fin_box = _get_fin_box(Fin(fin_x, fin_ys[0]), by_row) if len(fin_ys) == 1 or len(fin_ys) == 2 and \ _get_fin_box(Fin(fin_x, fin_ys[1]), by_row) == fin_box: y_ids = { y_id for y_id in all_y_ids if y_id not in fin_ys } if _check_if_sashimi(lines, x_ids, y_ids): cells = CELLS_IN_COL if by_row else CELLS_IN_ROW for y_id in y_ids: if fin_box.intersection( cells[y_id]): impacted_cells = impacted_cells.union( fin_box.intersection( cells[y_id])) impacted_cells = impacted_cells.difference( houses) to_eliminate = {(candidate, cell) for cell in impacted_cells if candidate in board[cell]} if to_eliminate: if window: solver_status.capture_baseline( board, window) impacted = { cell for cell in impacted_cells if len(board[cell]) > 1 } window.options_visible = window.options_visible.union( houses).union(impacted) kwargs["house"] = impacted.union(houses) 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, x_ids, by_row) kwargs["eliminate"] = to_eliminate return True
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 _color_wrap(): """ If in any house (row, column, box) there are two chain cells with the same color assigned to a value, then the value can be removed from all cells where it is marked with that color """ conflicted_cells = set() conflicting_color = set() def _check_houses(houses): impacted_nodes = conflicted_cells impacting_color = conflicting_color for i in range(9): house_nodes = component.intersection(houses[i]) if len(house_nodes) > 1: colors = set(pair[1] for node in house_nodes for pair in c_chain[node]) if len(colors) == 1: impacted_nodes = impacted_nodes.union(house_nodes) impacting_color = impacting_color.union(colors) elif len(house_nodes) > 2: assert (len(colors) == 2) nodes_with_color = defaultdict(set) for node in house_nodes: assert (len(c_chain[node]) == 1) vc_pair = c_chain[node].pop() c_chain[node].add(vc_pair) nodes_with_color[vc_pair[1]].add(node) for color in nodes_with_color: if len(nodes_with_color[color]) > 1: impacted_nodes = impacted_nodes.union( nodes_with_color[color]) impacting_color.add(color) return impacted_nodes, impacting_color conflicted_cells, conflicting_color = _check_houses(CELLS_IN_ROW) conflicted_cells, conflicting_color = _check_houses(CELLS_IN_COL) conflicted_cells, conflicting_color = _check_houses(CELLS_IN_BOX) if conflicted_cells: assert (len(conflicting_color) == 1) conflicting_color = conflicting_color.pop() to_eliminate = {(value, node) for node in component if (value, conflicting_color) in c_chain[node]} for node in conflicted_cells: c_chain[node] = {(value, 'red')} edges = [edge for edge in graph.edges if edge[0] in component] solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union( _get_graph_houses(edges)) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "color_wrap" kwargs["c_chain"] = c_chain kwargs["edges"] = edges kwargs["eliminate"] = to_eliminate simple_colors.options_removed += len(to_eliminate) simple_colors.clues += len(solver_status.naked_singles) return True return False
def _find_color_wrap(components, c_chains, m_id, s_id): color_nodes = _get_color_nodes(c_chains[m_id], value) s_edges = {edge for edge in graph.edges if edge[0] in components[s_id]} for edge in s_edges: for color in color_nodes: see_color_nodes_a = color_nodes[color].intersection( ALL_NBRS[edge[0]]) see_color_nodes_b = color_nodes[color].intersection( ALL_NBRS[edge[1]]) if see_color_nodes_a and see_color_nodes_b: edges = { edge for edge in graph.edges if edge[0] in components[m_id].union(components[s_id]) } to_eliminate = {(value, node) for node in color_nodes[color]} solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union( _get_graph_houses(edges)) eliminate_options(solver_status, board, to_eliminate, window) c_chain = {**c_chains[m_id], **c_chains[s_id]} for node in (see_color_nodes_a.pop(), see_color_nodes_b.pop()): c_chain[node] = {(value, 'red')} kwargs["solver_tool"] = "multi_colors-color_wrap" kwargs["c_chain"] = c_chain kwargs["edges"] = edges kwargs["eliminate"] = to_eliminate multi_colors.options_removed += len(to_eliminate) multi_colors.clues += len(solver_status.naked_singles) return True return False
def _color_trap(): """ two chain cells with different color are pointing at another cell outside of the chain that has a candidate equal to the value """ impacted_cells = [ cell for cell in range(81) if cell not in component and value in board[cell] and len(board[cell]) > 1 ] to_eliminate = [] for cell in impacted_cells: impacting_chain_nodes = component.intersection(set(ALL_NBRS[cell])) web_nodes_colors = set(pair[1] for node in impacting_chain_nodes for pair in c_chain[node]) if len(web_nodes_colors) > 1: assert (len(web_nodes_colors) == 2) to_eliminate.append((value, cell)) if to_eliminate: edges = [edge for edge in graph.edges if edge[0] in component] solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union( _get_graph_houses(edges)) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "color_trap" kwargs["c_chain"] = c_chain kwargs["edges"] = edges kwargs["eliminate"] = to_eliminate kwargs["impacted_cells"] = [pair[1] for pair in to_eliminate] simple_colors.options_removed += len(to_eliminate) simple_colors.clues += len(solver_status.naked_singles) return True return False
def _apply_brute_force(): """ try to resolve the sudoku puzzle by guessing an empty cell clue and then calling stack of standard techniques The sequence is repeated recursively until the puzzle is solved (or critical error occurs - then DeadEndException is being raised) """ next_cell, clue_iterator = _next_cell_to_resolve() if next_cell is None: return True def _restore_board(): for cell_id, value in enumerate(board_image_stack[-1]): board[cell_id] = value board_image_stack.append(board.copy()) iter_stack.append(clue_iterator) solver_status_stack.append(copy.deepcopy(solver_status)) window = data["graph_display"] for value in iter_stack[-1]: data["iter_counter"] += 1 solver_status.iteration = data["iter_counter"] _restore_board() solver_status.restore(solver_status_stack[-1]) to_eliminate = {(option, next_cell) for option in board[next_cell] if option != value} solver_status.capture_baseline(board, window) if window: window.options_visible.add(next_cell) eliminate_options(solver_status, board, to_eliminate, window) if config["output_opts"]["iterations"] and data[ "current_loop"] == config["repeat"] - 1: display.iteration(config, data, board, next_cell, value) if config["stats"]: data["current_path"].append(( data["current_loop"], next_cell // 9 + 1, next_cell % 9 + 1, value, board[next_cell], )) if window: window.draw_board(board, solver_tool="iterate", eliminate=to_eliminate, c_chain={next_cell: {(value, 'lime')}}) if _try_standard_techniques() and _apply_brute_force(): iter_stack.pop() board_image_stack.pop() solver_status_stack.pop() return True iter_stack.pop() _restore_board() board_image_stack.pop() solver_status_stack.pop() return False
def _type_4_5(): """ Two wing cells do not belong to the same house (row, column, or box). One of the cells has two candidates and the other one has two or three candidates. There is one common candidate Z in both cells. The other two wing cells form base: together they have three different candidates excluding Z (Type 4) or four candidates (Type 5). Selecting any base candidate either forces one of the first two cells to take Z value or creates XY-Wing (Type 4) or XYZ-Wing (Type 5) Rating: 240 (?) """ for wing in wings: base_cells = [ wing[0], ] for cell_id in wing[1:]: if len(set(ALL_NBRS[cell_id]).intersection(wing)) == 3: base_cells.append(cell_id) if len(base_cells) == 2: tmp = set(wing).difference(base_cells) cell_a = tmp.pop() cell_b = tmp.pop() z = set(board[cell_a]).intersection(board[cell_b]) conditions = [ bool(len(z) == 1), bool(len(board[cell_a]) == 2 or len(board[cell_b]) == 2), bool(len(board[cell_a]) <= 3 and len(board[cell_b]) <= 3), ] if all(conditions): z = z.pop() impacted_cells = _get_impacted_cells(wing, z) if impacted_cells: to_eliminate = {(z, cell_id) for cell_id in impacted_cells if z in board[cell_id]} if to_eliminate: base_candidates = set(''.join( board[cell_id] for cell_id in base_cells)) solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union( wing).union(impacted_cells) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "wxyz_wing_type_4" if len(base_candidates) == 3 else\ "wxyz_wing_type_5" kwargs["chain_d"] = _get_chain( board, base_cells, z) kwargs["chain_a"] = _get_chain( board, (cell_a, cell_b), z) kwargs["eliminate"] = to_eliminate kwargs["impacted_cells"] = { a_cell for _, a_cell in to_eliminate } wxyz_wing.options_removed += len(to_eliminate) wxyz_wing.clues += len(solver_status.naked_singles) # print(f'\t{kwargs["solver_tool"]}') return True return False
def _type_1(): """ Base position may contain 3 candidates (without Z value) or all 4 candidates (including Z). Each of the other wing cells contains two candidates, including Z that is common candidate for all three cells. Set of the sum of all candidates for the cells is of size 4. Rating: 200 (?) """ for wing in wings: if len(board[wing[0]]) > 2: for cell_id in wing[1:]: if len(set( ALL_NBRS[cell_id]).intersection(wing)) == 3 or len( board[cell_id]) != 2: break else: cell_a, cell_b, cell_c = _get_other_cells(wing) if len(_get_cells_candidates(wing[1:], common=False)) == 4: z = _get_cells_candidates(wing[1:]) if len(z) == 1: z = z.pop() if len(board[wing[0]]) == 4 or z not in board[ wing[0]]: impacted_cells = _get_impacted_cells(wing, z) if impacted_cells: to_eliminate = { (z, cell_id) for cell_id in impacted_cells if z in board[cell_id] } if to_eliminate: w = board[cell_c].replace(z, '') solver_status.capture_baseline( board, window) if window: window.options_visible = window.options_visible.union( wing).union(impacted_cells) eliminate_options( solver_status, board, to_eliminate, window) kwargs[ "solver_tool"] = "wxyz_wing_type_1" kwargs["chain_d"] = _get_chain( board, (wing[0], ), z, w) kwargs["chain_a"] = _get_chain( board, wing[1:], z, w) kwargs["eliminate"] = to_eliminate kwargs["impacted_cells"] = { a_cell for _, a_cell in to_eliminate } wxyz_wing.options_removed += len( to_eliminate) wxyz_wing.clues += len( solver_status.naked_singles) # print(f'\t{kwargs["solver_tool"]}') return True return False
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
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 als_xz(solver_status, board, window): """ The ALS-XZ rule says that if A and B are Almost Locked Sets (or ALS'es), and X is restricted common to A and B, then no other common candidate (let's call it Z) can appear outside of A and B in a cell that can see all the Z candidates in both A and B. Note that it doesn't matter whether or not Z is also restricted common to A and B, except that if Z is also restricted common, we can remove X as a candidate of any cells outside A and B that can see all the X candidates in both A and B. That is, each candidate, in turn, gets to play the role of restricted common, giving us the chance to eliminate the other candidate from outside cells. Rating: 300-350 """ als_es = _get_alses(board) for als_a, als_b in combinations(als_es, 2): common_values = set(als_a).intersection(als_b) cells_a = {cell for candidate in als_a for cell in als_a[candidate]} cells_b = {cell for candidate in als_b for cell in als_b[candidate]} impacted_cells = {cell for cell in range(81) if len(board[cell]) > 1 }.difference(cells_a).difference(cells_b) if len(common_values) > 1 and impacted_cells: for x in _get_restricted_commons(als_a, als_b, common_values): for z in common_values.difference((x, )): common_neighbours = _get_common_neighbours( als_a[z], als_b[z], impacted_cells) if common_neighbours: to_eliminate = {(z, cell) for cell in common_neighbours if z in board[cell]} if to_eliminate: chain_a, chain_b, _ = _get_c_chain(als_a=als_a, als_b=als_b, x=z, y=x) solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) if window: window.options_visible = window.options_visible.union( cells_a).union(cells_b).union( common_neighbours) als_xz.options_removed += len(to_eliminate) als_xz.clues += len(solver_status.naked_singles) return { "solver_tool": "als_xz", "chain_a": chain_a, "chain_b": chain_b, "eliminate": to_eliminate, "impacted_cells": {cell for _, cell in to_eliminate}, } return None
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
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 death_blossom(solver_status, board, window): """ Rating: 360 """ unresolved = {cell for cell in range(81) if len(board[cell]) > 1} als_es = _get_alses(board) for stem_size in (2, 3, 4): stems = {cell for cell in range(81) if len(board[cell]) == stem_size} for stem in stems: all_petals = {} for candidate in board[stem]: candidate_petals = _get_alses_with_restricted_common( stem, candidate, als_es) if candidate_petals: all_petals[candidate] = candidate_petals if len(all_petals) == len(board[stem]): stem_candidates = board[stem] for blossom in _select_2_petals(all_petals, stem_candidates) if stem_size == 2 \ else (_select_3_petals(all_petals, stem_candidates) if stem_size == 3 else _select_4_petals(all_petals, stem_candidates)): blossom_cells = { cell for petal in blossom for cell in petal } impacted_cells = unresolved.difference(blossom_cells) possible_candidates = set(blossom) to_eliminate = set() for cell in impacted_cells: if possible_candidates.intersection(board[cell]): cell_neighbours = set(ALL_NBRS[cell]) for candidate in possible_candidates.intersection( board[cell]): candidate_cells = blossom[candidate] if candidate_cells.intersection( cell_neighbours) == candidate_cells: to_eliminate.add((candidate, 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( blossom_cells) # print('\tDeath Blossom') return { "solver_tool": "death_blossom", "eliminate": to_eliminate, } return None
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 _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
def _find_color_wing(components, c_chains): color_nodes = [ _get_color_nodes(c_chains[0], value), _get_color_nodes(c_chains[1], value) ] for color_0 in color_nodes[0]: for node in color_nodes[0][color_0]: for color_1 in color_nodes[1]: if color_nodes[1][color_1].intersection(ALL_NBRS[node]): other_nodes = set(range(81)).difference( components[0].union(components[1])) other_nodes = { node for node in other_nodes if value in board[node] and len(board[node]) > 1 } color_a = _get_second_color(color_nodes[0], color_0) color_b = _get_second_color(color_nodes[1], color_1) impacted_cells = set() to_eliminate = [] for cell in other_nodes: if color_nodes[0][color_a].intersection(ALL_NBRS[cell]) \ and color_nodes[1][color_b].intersection(ALL_NBRS[cell]): impacted_cells.add(cell) to_eliminate.append((value, cell)) if to_eliminate: edges = [ edge for edge in graph.edges if edge[0] in components[0].union(components[1]) ] solver_status.capture_baseline(board, window) if window: window.options_visible = window.options_visible.union( _get_graph_houses(edges)) eliminate_options(solver_status, board, to_eliminate, window) kwargs["solver_tool"] = "multi_colors-color_wing" kwargs["c_chain"] = {**c_chains[0], **c_chains[1]} kwargs["edges"] = edges kwargs["eliminate"] = to_eliminate kwargs["impacted_cells"] = impacted_cells multi_colors.options_removed += len(to_eliminate) multi_colors.clues += len( solver_status.naked_singles) return True return False
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 _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
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 _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
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 _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
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
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
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
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
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 als_xy(solver_status, board, window): """ TODO Rating: 300 """ als_es = _get_alses(board) for als_a, als_b in combinations(als_es, 2): if _check_if_overlap(als_a, als_b): continue common_values = set(als_a).intersection(als_b) impacted_cells = {cell for cell in range(81) if len(board[cell]) > 1 }.difference(als_a).difference(als_b) if len(common_values) > 1 and impacted_cells: restricted_commons = _get_restricted_commons( als_a, als_b, common_values) if len(restricted_commons) == 2: to_eliminate = set() x = list(restricted_commons)[0] y = list(restricted_commons)[1] for cell in impacted_cells: if x in board[cell] and set(ALL_NBRS[cell]).intersection(als_a[x]) == als_a[x] and \ set(ALL_NBRS[cell]).intersection(als_b[x]) == als_b[x]: to_eliminate.add((x, cell)) if y in board[cell] and set(ALL_NBRS[cell]).intersection(als_a[y]) == als_a[y] and \ set(ALL_NBRS[cell]).intersection(als_b[y]) == als_b[y]: to_eliminate.add((y, cell)) # if to_eliminate: # print('\tBingo!') for z in set(als_a).difference(restricted_commons): for cell in impacted_cells: if z in board[cell] and set( ALL_NBRS[cell]).intersection( als_a[z]) == als_a[z]: to_eliminate.add((z, cell)) for z in set(als_b).difference(restricted_commons): for cell in impacted_cells: if z in board[cell] and set( ALL_NBRS[cell]).intersection( als_b[z]) == als_b[z]: to_eliminate.add((z, cell)) if to_eliminate: cells_a = { cell for candidate in als_a for cell in als_a[candidate] } cells_b = { cell for candidate in als_b for cell in als_b[candidate] } c_chain, d_chain, _ = _get_c_chain( als_a=als_a, als_b=als_b, x=restricted_commons.pop(), z=restricted_commons.pop()) solver_status.capture_baseline(board, window) eliminate_options(solver_status, board, to_eliminate, window) if window: window.options_visible = window.options_visible.union( cells_a).union(cells_b).union(impacted_cells) # print(f'\n{cells_a = }, \n{cells_b = }, \n{restricted_commons = }') als_xy.options_removed += len(to_eliminate) als_xy.clues += len(solver_status.naked_singles) return { "solver_tool": "als_xy", "chain_a": c_chain, "chain_b": d_chain, "eliminate": to_eliminate, "impacted_cells": { cell for _, cell in to_eliminate if cell not in cells_a.union(cells_b) }, } return None
def als_xy_wing(solver_status, board, window): """ Say we have three Almost Locked Sets A, B and C. Suppose: A and C share a restricted common Y, B and C share a restricted common Z, and Y and Z are different digits. Then for any digit X that is distinct from Y and Z and is a common candidate for A and B, we can eliminate X from any cell that sees all cells belonging to either A or B and having X as a candidate. Rating: 320-350 """ als_es = _get_alses(board) als_cells = {} unresolved = {cell for cell in range(81) if len(board[cell]) > 1} for idx_0, idx_1, idx_2 in combinations(range(len(als_es)), 3): common_values = set(als_es[idx_0]).intersection( als_es[idx_1]).intersection(als_es[idx_2]) if not common_values: continue if idx_0 not in als_cells: als_cells[idx_0] = { cell for candidate in als_es[idx_0] for cell in als_es[idx_0][candidate] } if idx_1 not in als_cells: als_cells[idx_1] = { cell for candidate in als_es[idx_1] for cell in als_es[idx_1][candidate] } if idx_2 not in als_cells: als_cells[idx_2] = { cell for candidate in als_es[idx_2] for cell in als_es[idx_2][candidate] } for idx_a, idx_b, idx_c in ((idx_0, idx_1, idx_2), (idx_1, idx_2, idx_0), (idx_0, idx_2, idx_1)): als_a = als_es[idx_a] cells_a = als_cells[idx_a] als_b = als_es[idx_b] cells_b = als_cells[idx_b] als_c = als_es[idx_c] cells_c = als_cells[idx_c] impacted_cells = unresolved.difference(cells_a).difference(cells_b) if impacted_cells: for y in _get_restricted_commons( als_a, als_c, set(als_a).intersection(als_c)): for z in _get_restricted_commons( als_b, als_c, set(als_b).intersection(als_c)): if y != z: for x in set(als_a).intersection(als_b).difference( {y, z}): common_neighbours = _get_common_neighbours( als_a[x], als_b[x], impacted_cells) if common_neighbours: to_eliminate = { (x, cell) for cell in common_neighbours if x in board[cell] } if to_eliminate: chain_a, chain_b, chain_c = _get_c_chain( als_a=als_a, als_b=als_b, als_c=als_c, x=x, y=y, z=z) impacted_cells = { cell for _, cell in to_eliminate }.difference(cells_a).difference( cells_b).difference(cells_c) solver_status.capture_baseline( board, window) eliminate_options( solver_status, board, to_eliminate, window) if window: window.options_visible = window.options_visible.union( cells_a).union(cells_b).union( cells_c).union( common_neighbours) als_xy_wing.options_removed += len( to_eliminate) als_xy_wing.clues += len( solver_status.naked_singles) return { "solver_tool": "als_xy_wing", "chain_a": chain_a, "chain_b": chain_b, "chain_c": chain_c, "eliminate": to_eliminate, "impacted_cells": impacted_cells, } return None