|
| 1 | +#!/usr/bin/env python |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | + |
| 4 | +# This file is part of Python Challenge Solutions |
| 5 | +# https://github.com/scorphus/PythonChallengeSolutions |
| 6 | + |
| 7 | +# Licensed under the BSD-3-Clause license: |
| 8 | +# https://opensource.org/licenses/BSD-3-Clause |
| 9 | +# Copyright (c) 2018-2020, Pablo S. Blum de Aguiar <scorphus@gmail.com> |
| 10 | + |
| 11 | +# http://www.pythonchallenge.com/pc/rock/arecibo.html |
| 12 | +# http://www.pythonchallenge.com/pc/rock/up.html |
| 13 | +# http://www.pythonchallenge.com/pc/rock/python.html |
| 14 | + |
| 15 | + |
| 16 | +from auth import get_last_href_url |
| 17 | +from auth import get_nth_comment |
| 18 | +from auth import read_riddle |
| 19 | +from cache import autocached |
| 20 | +from itertools import chain |
| 21 | +from itertools import product |
| 22 | +from itertools import zip_longest |
| 23 | +from pyeda.boolalg.expr import exprvar |
| 24 | +from pyeda.inter import And |
| 25 | +from pyeda.inter import Not |
| 26 | +from pyeda.inter import Or |
| 27 | + |
| 28 | + |
| 29 | +# A tuple with 62 different characters to be used in labeling variables |
| 30 | +AZ9 = ( |
| 31 | + tuple(map(chr, range(ord("A"), ord("Z") + 1))) |
| 32 | + + tuple(map(chr, range(ord("a"), ord("z") + 1))) |
| 33 | + + tuple(map(str, range(10))) |
| 34 | +) |
| 35 | + |
| 36 | + |
| 37 | +def load_puzzle(puzzle): |
| 38 | + """Load puzzle text input into a tuple of proposition lists""" |
| 39 | + horizontal, vertical = [], [] |
| 40 | + puzzle_it = iter(puzzle.splitlines()) |
| 41 | + for line in puzzle_it: |
| 42 | + if "horizontal" in line.lower(): |
| 43 | + break |
| 44 | + for line in puzzle_it: |
| 45 | + if "vertical" in line.lower(): |
| 46 | + break |
| 47 | + if line.rstrip(): |
| 48 | + horizontal.append(list(map(int, line.split()))) |
| 49 | + for line in puzzle_it: |
| 50 | + if line.rstrip(): |
| 51 | + vertical.append(list(map(int, line.split()))) |
| 52 | + return horizontal, vertical |
| 53 | + |
| 54 | + |
| 55 | +def expand(combination, size): |
| 56 | + """Expands a possible combination according to the desired size""" |
| 57 | + if len(combination) == size: |
| 58 | + yield combination |
| 59 | + else: |
| 60 | + for i in range((size - len(combination)) // 2 + 1): |
| 61 | + a = [0] * (size - len(combination) - i) |
| 62 | + b = [0] * i |
| 63 | + yield a + combination + b |
| 64 | + if a != b: |
| 65 | + yield b + combination + a |
| 66 | + |
| 67 | + |
| 68 | +def gen_all_combinations(proposition, size): |
| 69 | + """Generates all possible combinations of 1's and 0's of a given size |
| 70 | + according to a proposition""" |
| 71 | + rem = size - sum(proposition) |
| 72 | + parts = [[1] * i for i in proposition] |
| 73 | + seps = [[0] * i for i in range(1, rem + 1)] |
| 74 | + for sep in product(*([seps] * (len(proposition) - 1))): |
| 75 | + if sum(map(len, sep)) <= rem: |
| 76 | + yield from expand( |
| 77 | + list( |
| 78 | + chain.from_iterable( |
| 79 | + list(chain.from_iterable(t)) |
| 80 | + for t in zip_longest(parts, sep, fillvalue=[]) |
| 81 | + ) |
| 82 | + ), |
| 83 | + size, |
| 84 | + ) |
| 85 | + |
| 86 | + |
| 87 | +def gen_vars(size): |
| 88 | + """Generates expression variables for every cell in the grid and returns |
| 89 | + them as rows and columns""" |
| 90 | + chars = AZ9[:size] |
| 91 | + rows_vars = tuple( |
| 92 | + tuple(exprvar("".join(p)) for p in product(char, chars)) for char in chars |
| 93 | + ) |
| 94 | + return rows_vars, list(zip(*rows_vars)) |
| 95 | + |
| 96 | + |
| 97 | +def satisfy_all(rows_vars, cols_vars, horizontal, vertical): |
| 98 | + """Uses a SAT Solver to satisfy all horizontal and vertical propositions""" |
| 99 | + terms = [] |
| 100 | + for i, row_var in enumerate(rows_vars): |
| 101 | + row_terms = [] |
| 102 | + for combination in gen_all_combinations(horizontal[i], len(horizontal)): |
| 103 | + row_terms.append( |
| 104 | + And(*list(v if p else Not(v) for v, p in zip(row_var, combination))) |
| 105 | + ) |
| 106 | + terms.append(Or(*row_terms)) |
| 107 | + for i, col_var in enumerate(cols_vars): |
| 108 | + col_terms = [] |
| 109 | + for combination in gen_all_combinations(vertical[i], len(vertical)): |
| 110 | + col_terms.append( |
| 111 | + And(*list(v if p else Not(v) for v, p in zip(col_var, combination))) |
| 112 | + ) |
| 113 | + terms.append(Or(*col_terms)) |
| 114 | + return And(*terms).tseitin().satisfy_all() |
| 115 | + |
| 116 | + |
| 117 | +def sat_ip_to_text(sat_point, rows_vars): |
| 118 | + """Converts a satisfying input point into lines of text, one row per line""" |
| 119 | + text = "" |
| 120 | + for row_var in rows_vars: |
| 121 | + for var in row_var: |
| 122 | + if sat_point[var]: |
| 123 | + text += "▓▓" |
| 124 | + else: |
| 125 | + text += " " |
| 126 | + text += "\n" |
| 127 | + return text.rstrip() |
| 128 | + |
| 129 | + |
| 130 | +@autocached |
| 131 | +def solve_puzzle(puzzle_url): |
| 132 | + """Solves puzzle in `puzzle_url` and return all possible solutions""" |
| 133 | + puzzle = read_riddle(puzzle_url) |
| 134 | + horizontal, vertical = load_puzzle(puzzle) |
| 135 | + assert len(horizontal) == len(vertical) |
| 136 | + rows_vars, cols_vars = gen_vars(len(horizontal)) |
| 137 | + return [ |
| 138 | + sat_ip_to_text(ip, rows_vars) |
| 139 | + for ip in satisfy_all(rows_vars, cols_vars, horizontal, vertical) |
| 140 | + ] |
| 141 | + |
| 142 | + |
| 143 | +url = "http://www.pythonchallenge.com/pc/rock/arecibo.html" |
| 144 | +puzzle_path = get_nth_comment(url, 2).rsplit(maxsplit=1)[-1] |
| 145 | +url_base = url.rsplit("/", 1)[0] |
| 146 | +puzzle_url = f"{url_base}/{puzzle_path}" |
| 147 | +print("\n".join(solve_puzzle(puzzle_url))) |
| 148 | + |
| 149 | +url = "http://www.pythonchallenge.com/pc/rock/up.html" |
| 150 | +puzzle_url = get_last_href_url(url) |
| 151 | +print("\n".join(solve_puzzle(puzzle_url))) |
| 152 | + |
| 153 | +url = "http://www.pythonchallenge.com/pc/rock/python.html" |
| 154 | +for line in read_riddle(url).splitlines(): |
| 155 | + if "<" not in line: |
| 156 | + print(line.strip()) |
| 157 | + break |
0 commit comments