Skip to content

Commit a15c390

Browse files
committed
Add solution to mission 32
1 parent e41e867 commit a15c390

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed

32-arecibo.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
# Copyright (c) 2018-2020, Pablo S. Blum de Aguiar <scorphus@gmail.com>
77

88
pillow
9+
pyeda
910
pypng
1011
wand

0 commit comments

Comments
 (0)