Beispiel #1
0
    #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
Beispiel #4
0
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