def solve_board(board, expanded_colors, verbose=False): # Identify the input colors and mixing rules used based on the level. input_colors = HARD_INPUT_COLORS if expanded_colors else EASY_INPUT_COLORS mixing_rules = HARD_MIXING_RULES if expanded_colors else EASY_MIXING_RULES try: if RectBoard.is_rect_board(board): RectBoard.validate(board) else: HexBoard.validate(board) validate_colors(board, input_colors, mixing_rules) except ValueError as err: print('Board validation failed: %s' % err) return # Create the solver. solver = pywrapcp.Solver('algemy') start = time.time() grid = {} for r, row in enumerate(board): for c, el in enumerate(row): if el.strip() == '': # A solvable position. grid[(r, c)] = solver.IntVar(0, len(input_colors), '<Row %i, Col %i>' % (r, c)) else: grid[(r, c)] = None # crystal position if RectBoard.is_rect_board(board): b = RectBoard(grid) else: b = HexBoard(grid) # Helper: converts an input color to the int var space. color_i = lambda c: input_colors.index(c) + 1 # Helper: converts an int var to the input color string. i_color = lambda i: input_colors[i - 1] # CONSTRAINT - crystal illumination for (r, c), el in grid.items(): if el is not None: continue # only look at crystals sight_points = list(b.find_point_sightlines(r, c)) or_rules = [] for mix_rule in mixing_rules[board[r][c]]: pos_colors = [s[1:] for s in mix_rule if s[0] == '+'] def get_pos_rules(): for color in pos_colors: yield solver.Sum(p == color_i(color) for p in sight_points) >= 1 neg_colors = [s[1:] for s in mix_rule if s[0] == '-'] def get_neg_rules(): for color in neg_colors: yield solver.Sum(p == color_i(color) for p in sight_points) == 0 comb_rules = list(get_pos_rules()) + list(get_neg_rules()) final_rule = solver.Sum(comb_rules) == len(comb_rules) # ALL or_rules.append(final_rule) solver.Add(solver.Sum(or_rules) > 0) # ANY # CONSTRAINT - board sightlines for sightline in b.find_board_sightlines(): # At one item can be set (non-zero) in a sightline. solver.Add(solver.Sum(x > 0 for x in sightline) <= 1) # CONSTRAINT - all points solved for (r, c), el in grid.items(): if el is None: continue # ignore crystals # Either the point is non-empty or a point in its sightline is non-empty. solver.Add(el + solver.Sum(b.find_point_sightlines(r, c)) > 0) all_vars = list(filter(None, grid.values())) vars_phase = solver.Phase(all_vars, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT) if verbose: print("Time setting up constraints: %.2fms" % ((time.time() - start) * 1000)) solution = solver.Assignment() solution.Add(all_vars) collector = solver.FirstSolutionCollector(solution) start = time.time() solver.Solve(vars_phase, [collector]) if verbose: print("Solve time: %.2fms" % (1000 * (time.time() - start))) if collector.SolutionCount() < 1: print("\nNO SOLUTION FOUND") return None # TODO iterate over all solutions instead of using collector. # Allow finding first one then prompt to find next or maybe all. def lookup(r, c): el = grid[(r, c)] if el is None: return ' ' s = int(collector.Value(0, el)) if not s: return ' ' return i_color(s) solution = [] for r, row in enumerate(board): solution.append([lookup(r, c) for c in range(len(row))]) return solution