#file_name = "webpbn-01611-For merilinnuke" +'.txt' file_name = 'puzzles/' + file_name runs_row, runs_col, solution = decode(file_name) puzzle = Nonogram(runs_row, runs_col) find_solution = True make_guess = True # set solution if solution and not find_solution: # the webpbn files have solutions print("setting solution ...") grid_sol = decode_solution(solution, len(runs_row), len(runs_col)) puzzle.set_grid(grid_sol) ##solve game if find_solution: start_time = time.time() grid = solve_fast(puzzle, make_guess=make_guess) #grid = solve(puzzle) end_time = time.time() puzzle.set_grid(grid) #puzzle.show_grid(show_instructions=False, symbols="x#.?") # these are very big to print on the command line print("time taken: {:.5f}s".format(end_time - start_time)) print(puzzle.is_complete(), "{:.2f}%%".format(puzzle.progress * 100)) plot_nonogram(puzzle.grid, show_instructions=True, runs_row=runs_row, runs_col=runs_col) plt.show()
## set solution #puzzle.set_grid(solution) ## solve game start_time = time.time() grid = solve(puzzle) puzzle.set_grid(grid) end_time = time.time() puzzle.show_grid(show_instructions=True, to_file=False, symbols="x#.?") print(puzzle.is_complete(), "{:.2f}%%".format(puzzle.progress * 100)) print("time taken: {:.5f}s".format(end_time - start_time)) print("solved with {} guesses".format(puzzle.guesses)) plot_nonogram(puzzle.grid) if 1 == 1: start_time = time.time() #filename = 'rosetta_code_puzzles.txt' filename = "activity_workshop_puzzles.txt" ## https://activityworkshop.net/puzzlesgames/nonograms with open(filename) as file: lines = file.read().split("\n") for i in range(1, len(lines), 3): s = lines[i + 1].strip().split(" ") r_row = [ tuple(ord(c) - ord('A') + 1 for c in run) for run in s ] s = lines[i + 2].strip().split(" ") r_col = [ tuple(ord(c) - ord('A') + 1 for c in run) for run in s
def solve(nonogram_): "Do constraint propagation before exhaustive search. Based on the RosettaCode code" exhaustive_search_max_iters = 1e6 def initialise(length, runs): """If any sequence x in the run is greater than the number of free whites, some these values will be fixed""" # The first run of fix_row() or fix_col() will find this anyway. But this is faster arr = [EITHER] * length free_whites = length - sum(runs) - (len(runs) - 1 ) # remaining whites to place j = 0 # current position for x in runs: if x > free_whites: # backfill s for c in range(j + free_whites, j + x): arr[c] = BLACK if (free_whites == 0) and (j + x < length): arr[j + x] = WHITE # can place a white too j += x + 1 return arr def fits(a, b): """ Use binary to represent white and black 01 -> black, 10 -> white, 11 -> either black & white == 0 -> invalid black & black == black white & white == white black & 3 == either white & 3 == either """ return all(x & y for x, y in zip(a, b)) def generate_sequences(fixed, runs): """ Generates valid sequences. """ length = len(fixed) n = len(runs) sequence_whites = [0] + [1] * (n - 1) + [ 0 ] # at least one white in between each free_whites = length - sum(runs) - (n - 1) # remaining whites to place possible = [] # all possible permutations of the run # find all ways to place remaining free whites for partition in unique_perm_partitions( free_whites, len(sequence_whites)): # unique partitions arr = [] for n_white, n_free, n_black in zip(sequence_whites, partition, runs): arr += [WHITE] * (n_white + n_free) + ([BLACK] * n_black) # place last set of whites arr += [WHITE] * (sequence_whites[n] + partition[n]) if fits(arr, fixed): # there are no conflicts possible.append(arr) return possible def find_allowed(possible_arrays): """Finds all allowed value in the set of possible arrays This is done with binary OR. Values that are always 1 or 2 will never change. If either, they will become a 3 If they remain 0 -> there is no solution """ allowed = [0] * len(possible_arrays[0]) for array in possible_arrays: allowed = [x | y for x, y in zip(allowed, array)] return allowed def fix_row(i): fixed = grid[i] possible_rows[i] = [ row for row in possible_rows[i] if fits(fixed, row) ] # reduce the amount of possible rows allowed = find_allowed(possible_rows[i]) for j in range(n_cols): if fixed[j] != allowed[j]: columns_to_edit.add(j) grid[i] = allowed def fix_col(j): fixed = [grid[i][j] for i in range(n_rows)] possible_cols[j] = [ col for col in possible_cols[j] if fits(fixed, col) ] # reduce the amount of possible cols allowed = find_allowed(possible_cols[j]) for i in range(n_rows): if fixed[i] != allowed[i]: rows_to_edit.add(i) grid[i][j] = allowed[i] # extract values from Nonogram object n_rows, n_cols = nonogram_.n_rows, nonogram_.n_cols runs_row, runs_col = nonogram_.runs_row, nonogram_.runs_col grid = [row[:] for row in nonogram_.grid] # initialise plot save, instruct, plot_progess = True, True, False # seriously slows down code if plot_progess: ax = plot_nonogram(grid, save=save, filename="solving_sweep_0", show_instructions=instruct, runs_row=runs_row, runs_col=runs_col) else: ax = None # initialise rows and columns. This reduces the amount of valid configurations for generate_sequences for i in range(n_rows): grid[i] = initialise(n_cols, runs_row[i]) for j in range(n_cols): col = initialise(n_rows, runs_col[j]) for i in range(n_rows): grid[i][j] = col[i] update_nonogram_plot(grid, ax=ax, save=save, filename="solving_sweep_2", plot_progess=plot_progess) # generate ALL possible sequences. SLOW and MEMORY INTENSIVE possible_rows = [ generate_sequences(grid[i], runs_row[i]) for i in range(n_rows) ] possible_cols = [] for j in range(n_cols): col = [grid[i][j] for i in range(n_rows)] possible_cols.append(generate_sequences(col, runs_col[j])) print("initial") n_possible_rows = [len(x) for x in possible_rows] n_possible_cols = [len(x) for x in possible_cols] print("possible rows: {}\npossible columns: {}".format( n_possible_rows, n_possible_cols)) print("summary: {} possible rows and {} possible columns".format( sum(n_possible_rows), sum(n_possible_cols))) # rows, columns for constraint propagation to be applied rows_to_edit = set(range(n_rows)) columns_to_edit = set(range(n_cols)) sweeps = 2 # include initialising while columns_to_edit: # constraint propagation for j in columns_to_edit: fix_col(j) sweeps += 1 update_nonogram_plot(grid, ax=ax, save=save, filename="solving_sweep_{}".format(sweeps), plot_progess=plot_progess) columns_to_edit = set() for i in rows_to_edit: fix_row(i) sweeps += 1 update_nonogram_plot(grid, ax=ax, save=save, filename="solving_sweep_{}".format(sweeps), plot_progess=plot_progess) rows_to_edit = set() print("\nconstraint propagation done in {} rounds".format(sweeps)) n_possible_rows = [len(x) for x in possible_rows] n_possible_cols = [len(x) for x in possible_cols] print("possible rows: {}\npossible columns: {}".format( n_possible_rows, n_possible_cols)) print("summary: {} possible rows and {} possible columns".format( sum(n_possible_rows), sum(n_possible_cols))) possible_combinations = 1 for x in n_possible_rows: possible_combinations *= x print(" {:e} possibile combinations".format(possible_combinations)) print("") solution_found = all(grid[i][j] in (BLACK, WHITE) for j in range(n_cols) for i in range(n_rows)) # might be incorrect if solution_found: print("Solution is unique") # but could be incorrect! elif possible_combinations >= exhaustive_search_max_iters: print("Not trying exhaustive search. Too many possibilities") else: print("Solution may not be unique, doing exhaustive search:") def try_all(grid, i=0): if i >= n_rows: for j in range(n_cols): col = [row[j] for row in grid] if col not in possible_cols[j]: return 0 nonogram_.show_grid(grid) print("") return 1 sol = 0 for row in possible_rows[i]: grid[i] = row if nonogram_.is_valid_partial_columns(grid): grid_next = [row[:] for row in grid] sol += try_all(grid_next, i + 1) return sol # start exhaustive search if not solved if not solution_found and possible_combinations < exhaustive_search_max_iters: grid_next = [row[:] for row in grid] num_solutions = try_all(grid_next, i=0) if num_solutions == 0: print("No solutions found") elif num_solutions == 1: print("Unique solution found") else: # num_solutions > 1: print("{} solutions found".format(num_solutions)) return grid
def solve_fast_(grid, nonogram_, rows_to_edit=None, columns_to_edit=None, make_guess=False, depth=0, depth_max=None): "Solve using logic and constraint propagation. Resort to guessing if this doesn't work" if depth_max is not None and depth > depth_max: raise SolvingError("maximum recursion depth reached") # important: pass one Nonogram object around but a separate grid object is edited on each recursive call def simple_filler(arr, runs): """ fill in black gaps and whites ending sequences. The overlap algorithm might miss these""" k = 0 # index for runs on_black = 0 allowed = arr[:] for i in range(len(arr)): if arr[i] == WHITE: if on_black > 0: k += 1 # move to the next pattern on_black = 0 elif arr[i] == BLACK: on_black += 1 else: # arr[i] == EITHER if k >= len(runs): break elif (0 < on_black < runs[k]): # this must be part of this sequence allowed[i] = BLACK on_black += 1 elif on_black == 0 and k > 0 and i > 0 and arr[ i - 1] == BLACK: # this must be a white ending a sequence allowed[i] = WHITE else: break # too many unknowns # put whites next to any 1 runs. Very special case if all([r == 1 for r in runs]): for i in range(len(arr)): if arr[i] == BLACK: if i > 0: allowed[i - 1] = WHITE if i < len(arr) - 1: allowed[i + 1] = WHITE return allowed def changer_sequence(vec): """ convert to ascending sequence """ counter = int(vec[0] == BLACK) prev = vec[0] sequence = [counter] for x in vec[1:]: counter += (prev != x ) # increase by one every time a new sequence starts sequence.append(counter) prev = x return sequence def overlap(a, b): out = [] for x, y in zip(changer_sequence(a), changer_sequence(b)): if x == y: if (x + 2) % 2 == 0: out.append(WHITE) else: out.append(BLACK) else: out.append(EITHER) return out def left_rightmost_overlap(arr, runs): """Returns the overlap between the left-most and right-most fitting sequences""" left = nonogram_.NFA.find_match(arr, runs) right = nonogram_.NFA.find_match(arr[::-1], runs[::-1]) if left.is_match and right.is_match: allowed = overlap(left.match, right.match[::-1]) else: raise SolvingError( "Left or right most match not found. A mistake was made") return allowed def splitter(arr, runs): """split rows at the max element. Then the strategies can be applied to each division. This helps more with speed than with solving, because it speeds up the matching algorithm.""" if not arr or not runs: return [(arr, runs)] runs_, positions = nonogram_._get_sequence(arr) split_value = max(runs) split_idx = runs.index(split_value) if runs.count(split_value) == 1 and split_value in runs_: idx = runs_.index(split_value) i0, i1 = positions[idx] i0, i1 = max(i0 - 1, 0), min(i1 + 1, len(arr)) # add whites on either side return splitter(arr[:i0], runs[:split_idx]) + [ (arr[i0:i1 + 1], (runs[split_idx], )) ] + splitter(arr[i1 + 1:], runs[split_idx + 1:]) else: return [(arr, runs)] def apply_strategies(array, runs): # allowed_full = [EITHER] * len(array) # allowed_full = [x & y for x, y in zip(allowed_full, left_rightmost_overlap(tuple(array), tuple(runs)))] # allowed_full = [x & y for x, y in zip(allowed_full, simple_filler(array, runs))] # allowed_full = [x & y for x, y in zip(allowed_full, simple_filler(array[::-1], runs[::-1])[::-1])] allowed_full = [] for split in splitter(array, runs): segment, runs_segment = split if not segment: continue allowed = [EITHER] * len(segment) allowed = [ x & y for x, y in zip( allowed, left_rightmost_overlap(tuple(segment), tuple( runs_segment))) ] allowed = [ x & y for x, y in zip(allowed, simple_filler(segment, runs_segment)) ] allowed = [ x & y for x, y in zip( allowed, simple_filler(segment[::-1], runs_segment[::-1])[::-1]) ] # going from right allowed_full.extend(allowed) return allowed_full def fix_row(i): row = grid[i] allowed = apply_strategies(row, runs_row[i]) for j in range(n_cols): if row[j] != allowed[j] and allowed[j] != EITHER: columns_to_edit.add(j) grid[i] = allowed def fix_col(j): col = [grid[i][j] for i in range(n_rows)] allowed = apply_strategies(col, runs_col[j]) for i in range(n_rows): if col[i] != allowed[i] and allowed[i] != EITHER: rows_to_edit.add(i) grid[i][j] = allowed[i] # extract values from Nonogram object n_rows, n_cols = nonogram_.n_rows, nonogram_.n_cols runs_row, runs_col = nonogram_.runs_row, nonogram_.runs_col # initialise plot save, instruct, plot_progess = True, True, False # seriously slows down code if plot_progess: ax = plot_nonogram(grid, save=save, filename="solving_sweep_0", show_instructions=instruct, runs_row=runs_row, runs_col=runs_col) else: ax = None if rows_to_edit is None and columns_to_edit is None: # initialise # rows, columns for constraint propagation to be applied rows_to_edit = set() columns_to_edit = set(range(n_cols)) for i in range(n_rows): fix_row(i) sweeps = 1 # include initialise else: sweeps = 0 while not nonogram_.is_complete(grid): # constraint propagation while columns_to_edit: for j in columns_to_edit: fix_col(j) sweeps += 1 update_nonogram_plot(grid, ax=ax, save=save, filename="solving_sweep_{}".format(sweeps), plot_progess=plot_progess) columns_to_edit = set() for i in rows_to_edit: fix_row(i) sweeps += 1 update_nonogram_plot(grid, ax=ax, save=save, filename="solving_sweep_{}".format(sweeps), plot_progess=plot_progess) if nonogram_.guesses == 0: print("constraint propagation done in {} sweeps".format(sweeps)) if not make_guess: break # guessing if not nonogram_.is_complete(grid) and make_guess: nonogram_.guesses += 1 # only the first one is a guess, the second time we know it is right progress = 1 - sum(row.count(EITHER) for row in grid) / (n_rows * n_cols) guess = probe(grid, nonogram_) if guess is None: raise SolvingError( "all guesses from this configuration are are wrong") i, j, values, rank, prog = guess if len(values) == 1: grid[i][j] = values[0] rows_to_edit = {i} columns_to_edit = {j} print("{} {:.5f}%".format(nonogram_.guesses - 1, progress * 100)) # try constraint propation again else: # make a guess print("{} {:.5f}% guess {:d}".format(nonogram_.guesses - 1, progress * 100, depth + 1)) for cell in values: grid_next = [row[:] for row in grid] grid_next[i][j] = cell try: grid_next = solve_fast_(grid_next, nonogram_, {i}, {j}, make_guess=True, depth=depth + 1) if nonogram_.is_complete(grid_next): return grid_next except SolvingError: pass return grid # all search paths exhausted return grid