Continuing from this post: The Python Challenge (Levels 14-19)!
Continuing with the Python Challenge!
First, the auth tuple.
auth = ('butter', 'fly')
Level 20¶
http://butter:fly@www.pythonchallenge.com/pc/hex/idiot2.html
Title: go away!
but inspecting it carefully is allowed.
There is no further information whatsoever in the source. Nothing at all. In fact, here’s the entire source:
<html>
<head>
<title>go away!</title>
<link rel="stylesheet" type="text/css" href="../style.css">
</head>
<body>
<br><br>
<center>
<font color="gold">
<img src="unreal.jpg" border="0"/><br><br>
but inspecting it carefully is allowed.
</body>
</html>
So yeah. Nothing. Let’s inspect the image then.
$ file unreal.jpg
unreal.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), density 72x72, segment length 16, Exif Standard: [TIFF image data, big-endian, direntries=0], baseline, precision 8, 290x478, components 3
Nothing weird at all. I’m quite very stuck.
I’m not gonna lie, this is when I Googled “pythonchallenge 20”. And the first result said that the image is a huge file and only a small portion is being served. OK!
Opening the “Network” panel of my devtools, indeed this is staring at me in “Response Headers” of the image file:
Content-Range: bytes 0-30202/2123456789
So what if we get more of the file?
import requests
r = requests.get(
'http://www.pythonchallenge.com/pc/hex/unreal.jpg',
auth=auth,
headers={'Range': 'bytes=30203-2123456789'},
)
r.content
b"Why don't you respect my privacy?\n"
Because I’m doing the Python Challenge. I’m sorry…
curpos = 30203 + len(r.content)
r = requests.get(
'http://www.pythonchallenge.com/pc/hex/unreal.jpg',
auth=auth,
headers={'Range': f'bytes={curpos}-2123456789'},
)
curpos, r.content
(30237, b'we can go on in this way for really long time.\n')
curpos += len(r.content)
r = requests.get(
'http://www.pythonchallenge.com/pc/hex/unreal.jpg',
auth=auth,
headers={'Range': f'bytes={curpos}-2123456789'},
)
curpos, r.content
(30284, b'stop this!\n')
curpos += len(r.content)
r = requests.get(
'http://www.pythonchallenge.com/pc/hex/unreal.jpg',
auth=auth,
headers={'Range': f'bytes={curpos}-2123456789'},
)
curpos, r.content
(30295, b'invader! invader!\n')
curpos += len(r.content)
r = requests.get(
'http://www.pythonchallenge.com/pc/hex/unreal.jpg',
auth=auth,
headers={'Range': f'bytes={curpos}-2123456789'},
)
curpos, r.content
(30313, b'ok, invader. you are inside now. \n')
curpos += len(r.content)
r = requests.get(
'http://www.pythonchallenge.com/pc/hex/unreal.jpg',
auth=auth,
headers={'Range': f'bytes={curpos}-2123456789'},
)
curpos, r.content
(30347, b'')
Welp… What now?
r.headers
{'Content-type': 'text/html; charset=UTF-8', 'Content-Length': '0', 'Date': 'Sat, 27 Jan 2024 04:45:09 GMT', 'Server': 'lighttpd/1.4.55'}
Nothing in the headers… What about the previous request?
curpos = 30313
r = requests.get(
'http://www.pythonchallenge.com/pc/hex/unreal.jpg',
auth=auth,
headers={'Range': f'bytes={curpos}-2123456789'},
)
r.content, r.headers
(b'ok, invader. you are inside now. \n', {'Content-Type': 'application/octet-stream', 'Content-Transfer-Encoding': 'binary', 'Content-Range': 'bytes 30313-30346/2123456789', 'Content-Length': '34', 'Date': 'Sat, 27 Jan 2024 04:45:10 GMT', 'Server': 'lighttpd/1.4.55'})
Still nothing…
Looking at the end of the range again, it looks like a carefully crafted number (2 + 123456789), and it’s really close to $2^{31}$ (2147483648). Maybe there’s something in the back?
r = requests.get(
'http://www.pythonchallenge.com/pc/hex/unreal.jpg',
auth=auth,
headers={'Range': f'bytes=2123456789-2147483648'},
)
r.content, r.headers
(b'esrever ni emankcin wen ruoy si drowssap eht\n', {'Content-Type': 'application/octet-stream', 'Content-Transfer-Encoding': 'binary', 'Content-Range': 'bytes 2123456744-2123456788/2123456789', 'Content-Length': '45', 'Date': 'Sat, 27 Jan 2024 04:45:11 GMT', 'Server': 'lighttpd/1.4.55'})
Oh that’s something…
r.text[::-1]
'\nthe password is your new nickname in reverse'
Well my new nickname is probably “invader”. So in reverse, that’s “redavni”. But http://www.pythonchallenge.com/pc/hex/redavni.html is a 404 error… Maybe there’s more data?
r = requests.get(
'http://www.pythonchallenge.com/pc/hex/unreal.jpg',
auth=auth,
headers={'Range': f'bytes=2123456743-'},
)
r.content, r.headers
(b'and it is hiding at 1152983631.\n', {'Content-Type': 'application/octet-stream', 'Content-Transfer-Encoding': 'binary', 'Content-Range': 'bytes 2123456712-2123456743/2123456789', 'Content-Length': '32', 'Date': 'Sat, 27 Jan 2024 04:45:11 GMT', 'Server': 'lighttpd/1.4.55'})
Great, another clue!
r = requests.get(
'http://www.pythonchallenge.com/pc/hex/unreal.jpg',
auth=auth,
headers={'Range': f'bytes=1152983631-'},
)
r.content[:100], r.headers
(b'PK\x03\x04\x14\x00\t\x00\x08\x00;\xa7\xaa2\xac\xe5f\x14\xa9\x00\x00\x00\xd3\x00\x00\x00\n\x00\x15\x00readme.txtUT\t\x00\x03"\xf6\x80B\x19\xf7\x80BUx\x04\x00\xe8\x03\xe8\x03R\x1d^\xf1\xe5\xbf\xa3\xc2\xc0]\xc2)\xfd|\xdbC\x9b\xa5\xf6B\xc1j\x1c\x8cJ^6VE\x87\xcd\xaa\x1e\xf3\xd5P\xc4\xb5I', {'Content-Type': 'application/octet-stream', 'Content-Transfer-Encoding': 'binary', 'Content-Range': 'bytes 1152983631-1153223363/2123456789', 'Content-Length': '239733', 'Date': 'Sat, 27 Jan 2024 04:45:12 GMT', 'Server': 'lighttpd/1.4.55'})
Ah, the PK at the beginning means it’s a ZIP file!
import io
import zipfile
data = r.content
zf = zipfile.ZipFile(io.BytesIO(data))
zf.filelist
[<ZipInfo filename='readme.txt' compress_type=deflate filemode='-rw-r--r--' file_size=211 compress_size=169>, <ZipInfo filename='package.pack' compress_type=deflate filemode='-rw-r--r--' file_size=239194 compress_size=239246>]
zippwd = b'redavni'
with zf.open('readme.txt', pwd=zippwd) as f:
print(f.read().decode())
Yes! This is really level 21 in here. And yes, After you solve it, you'll be in level 22! Now for the level: * We used to play this game when we were kids * When I had no idea what to do, I looked backwards.
Annnnnnd hooray!
Level 21¶
http://butter:fly@www.pythonchallenge.com/pc/hex/unreal.jpg (Range: bytes=1152983631-)
ZIP password: redavni
Yes! This is really level 21 in here. And yes, After you solve it, you’ll be in level 22!
Now for the level:
- We used to play this game when we were kids
- When I had no idea what to do, I looked backwards.
First let’s display the contents of the ZIP file again:
zf.infolist()
[<ZipInfo filename='readme.txt' compress_type=deflate filemode='-rw-r--r--' file_size=211 compress_size=169>, <ZipInfo filename='package.pack' compress_type=deflate filemode='-rw-r--r--' file_size=239194 compress_size=239246>]
The text above is the contents of “readme.txt”. Now what’s this “package.pack”?
with zf.open('package.pack', pwd=zippwd) as f:
print(f.read()[:50])
b'x\x9c\x00\n@\xf5\xbfx\x9c\x00\x07@\xf8\xbfx\x9c\x00\x06@\xf9\xbfx\x9c\x00\xff?\x00\xc0x\x9c\x00\xff?\x00\xc0x\x9c\x84vuT\x14N\xd46\xddH#\xad\x80'
I can’t tell by my naked eye what this is…
# You can skip this cell if python-magic is already installed
%pip install python-magic
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple Requirement already satisfied: python-magic in ./.venv/lib/python3.11/site-packages (0.4.27) Note: you may need to restart the kernel to use updated packages.
import magic
with zf.open('package.pack', pwd=zippwd) as f:
print(magic.from_buffer(f.read()))
zlib compressed data
“zlib compressed data”. OK let’s decompress it then!
import zlib
# should have done this long ago :/
with zf.open('package.pack', pwd=zippwd) as f:
data = f.read()
data = zlib.decompress(data)
magic.from_buffer(data)
'zlib compressed data'
Another one?!
data = zlib.decompress(data)
magic.from_buffer(data)
'zlib compressed data'
Hmm ok, let’s loop it then!
while True:
try:
data = zlib.decompress(data)
except:
break
print(len(data))
238951 238870 238789 238759
magic.from_buffer(data)
'bzip2 compressed data, block size = 900k'
OK, now bzip2? Sure:
import bz2
while True:
try:
data = bz2.decompress(data)
except:
break
print(len(data))
237334 235892 234604
magic.from_buffer(data)
'zlib compressed data'
Uhhhh…
import bz2
while True:
try:
data = bz2.decompress(data)
except:
pass
else:
continue
try:
data = zlib.decompress(data)
except:
break
OK maybe this is finally it…?
magic.from_buffer(data)
'data'
Good! Let’s see what it is:
data[:100]
b'\x80\x8d\x96\xcb\xb5r\xa7\x00\x06Xz\xdafO\x19\xee\x84k\xa4dAB\xe1\x14\xc9]\xfc\xffT!\xd0\xce3f\xff\xdd\x89\xdd\xa5Y_\x85\x0c%[M8\x89U,\xb1\xd9g\xe1\x13\x04\x12\x16\x85u\xaep\xff\xd1\xb52\xeb\xaav\x11t\xe8\xd1\xdc\x043V\xd6s1\xc7\xa9\xe8\x91\x85\xd0\xdf\xf0s~^\xdb\xd9\xab\x08\\\x1a\x0fQ\xd1'
Welp, still garbage… What can it possibly be? I have no idea what to do.
Then I remembered that the question statement said: “When I had no idea what to do, I looked backwards.”
Aha, so can I reverse the bytestring?
last = True
data = data[::-1]
while True:
try:
data = bz2.decompress(data)
except:
pass
else:
last = False
continue
try:
data = zlib.decompress(data)
except:
if last:
break
last = True
data = data[::-1]
else:
last = False
17! I like it. Let’s view the string:
data
b'look at your logs'
Look at my… logs? I can only guess that it refers to my logs of decompressing again and again the data. So is the logs referring to the sequence of numbers?
# Reload the data from the ZIP archive
with zf.open('package.pack', pwd=zippwd) as f:
data = f.read()
# Since I know the final string is 17B I can cheat a bit :D
lst = []
while True:
try:
data = bz2.decompress(data)
except:
try:
data = zlib.decompress(data)
except:
data = data[::-1]
lst.append(len(data))
if len(data) == 17:
break
len(lst), lst[:10], lst[-10:]
(741, [239113, 239032, 238951, 238870, 238789, 238759, 237334, 235892, 234604, 234523], [253, 179, 168, 160, 149, 138, 127, 116, 53, 17])
OK, I don’t see anything special about these numbers. But wait… What if the “logs” refer to the decompression method used each time? That could make up a bitstring! Let’s see:
with zf.open('package.pack', pwd=zippwd) as f:
data = f.read()
od = ''
while True:
try:
data = bz2.decompress(data)
od += 'B'
except:
try:
data = zlib.decompress(data)
od += 'Z'
except:
data = data[::-1]
od += 'R'
if len(data) == 17:
break
len(od), od
(741, 'ZZZZZZBBBZZZZZZZZZZBBBZZZZZZBBBBBBBBZZZZBBBBBBBBZZZZBBBBBBBBBBZZBBBBBBBBRZZZZBBBBBBBZZZZZZBBBBBBBZZZZBBBBBBBBBZZZBBBBBBBBBZZZBBBBBBBBBZZZBBBBBBBBBRZZZBBZZZZZBBZZZZBBZZZZZBBZZZBBZZZZZZBBZZBBZZZZZZBBZZBBZZZZZZZZZZBBZZZZZZBBRZZBBZZZZZZZZZZZBBZZZZZZZBBZZBBZZZZZZBBZZBBZZZZZZBBZZBBZZZZZZZZZZBBZZZZZZBBRZZBBZZZZZZZZZZZBBZZZZZZZBBZZBBBBBBBBBZZZBBBBBBBBBZZZBBBBBBBBZZZZBBBBBBBBBRZZBBZZZZZZZZZZZBBZZZZZZZBBZZBBBBBBBBZZZZBBBBBBBBZZZZBBBBBBBBZZZZBBBBBBBBZRZZBBZZZZZZZZZZZBBZZZZZZZBBZZBBZZZZZZZZZZBBZZZZZZZZZZBBZZZZZZZZZZBBZZZBBZRZZZBBZZZZZBBZZZZBBZZZZZBBZZZBBZZZZZZZZZZBBZZZZZZZZZZBBZZZZZZZZZZBBZZZZBBZRZZZZBBBBBBBZZZZZZBBBBBBBZZZZBBZZZZZZZZZZBBZZZZZZZZZZBBBBBBBBBZZZBBZZZZZBBZRZZZZZZBBBZZZZZZZZZZBBBZZZZZZBBZZZZZZZZZZBBZZZZZZZZZZBBBBBBBBBBZZBBZZZZZZBB')
OK. Problem: 741 is not a multiple of 8, so it can’t be a series of bytes. What can it be then?
I noticed that there aren’t a lot of R’s. How many are there?
od.count('R')
9
Only 9! That could mean something. Let’s try splitting at the R’s:
od.split('R')
['ZZZZZZBBBZZZZZZZZZZBBBZZZZZZBBBBBBBBZZZZBBBBBBBBZZZZBBBBBBBBBBZZBBBBBBBB', 'ZZZZBBBBBBBZZZZZZBBBBBBBZZZZBBBBBBBBBZZZBBBBBBBBBZZZBBBBBBBBBZZZBBBBBBBBB', 'ZZZBBZZZZZBBZZZZBBZZZZZBBZZZBBZZZZZZBBZZBBZZZZZZBBZZBBZZZZZZZZZZBBZZZZZZBB', 'ZZBBZZZZZZZZZZZBBZZZZZZZBBZZBBZZZZZZBBZZBBZZZZZZBBZZBBZZZZZZZZZZBBZZZZZZBB', 'ZZBBZZZZZZZZZZZBBZZZZZZZBBZZBBBBBBBBBZZZBBBBBBBBBZZZBBBBBBBBZZZZBBBBBBBBB', 'ZZBBZZZZZZZZZZZBBZZZZZZZBBZZBBBBBBBBZZZZBBBBBBBBZZZZBBBBBBBBZZZZBBBBBBBBZ', 'ZZBBZZZZZZZZZZZBBZZZZZZZBBZZBBZZZZZZZZZZBBZZZZZZZZZZBBZZZZZZZZZZBBZZZBBZ', 'ZZZBBZZZZZBBZZZZBBZZZZZBBZZZBBZZZZZZZZZZBBZZZZZZZZZZBBZZZZZZZZZZBBZZZZBBZ', 'ZZZZBBBBBBBZZZZZZBBBBBBBZZZZBBZZZZZZZZZZBBZZZZZZZZZZBBBBBBBBBZZZBBZZZZZBBZ', 'ZZZZZZBBBZZZZZZZZZZBBBZZZZZZBBZZZZZZZZZZBBZZZZZZZZZZBBBBBBBBBBZZBBZZZZZZBB']
OK, because of the way my editor is set up, I can see something there. In case it doesn’t display clearly for some people, let me make it clearer:
lst = od.split('R')
trans = str.maketrans({'Z': ' ', 'B': '@'})
for item in lst:
print(item.translate(trans))
@@@ @@@ @@@@@@@@ @@@@@@@@ @@@@@@@@@@ @@@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@ @@@@@@@@@ @@ @@ @@ @@@@@@@@ @@@@@@@@ @@@@@@@@ @@@@@@@@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@ @@@@@@@ @@@@@@@ @@ @@ @@@@@@@@@ @@ @@ @@@ @@@ @@ @@ @@@@@@@@@@ @@ @@
Well it sure as heck says “COPPER”. So here we go!
Level 22¶
http://butter:fly@www.pythonchallenge.com/pc/hex/copper.html
Title: emulate
In the source:
<!-- or maybe white.gif would be more bright-->
OK let’s take a look!
That is NOT bright AT ALL. Well let’s analyze it anyway!
import io
import requests
from PIL import Image
r = requests.get('http://www.pythonchallenge.com/pc/hex/white.gif', auth=auth)
im = Image.open(io.BytesIO(r.content))
repr(im)
'<PIL.GifImagePlugin.GifImageFile image mode=P size=200x200 at 0x7FBE880BD550>'
px = []
for i in range(im.height):
row = []
for j in range(im.width):
row.append(im.getpixel((j, i)))
px.append(row)
px[0][:20]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
At first glance it looks like it’s all zeroes. But it can’t be… Right?
for i in range(im.height):
for j in range(im.width):
if px[i][j] != 0:
print(i, j, px[i][j])
100 100 8
Ah, one of the pixels is not 0: the pixel at the center. But is that… it? Then I remembered that it’s a GIF file and it probably moves. Let’s see:
im.n_frames
133
Good! Let’s see what’s happening in frame 2:
im.seek(1)
px = []
for i in range(im.height):
row = []
for j in range(im.width):
row.append(im.getpixel((j, i)))
px.append(row)
px[0][:20]
[(0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)]
Hmm… Some weird reason seems to cause the pixels to become 3-tuples. That’s not the matter; what matters is that they’re all still almost all zero! Let’s check for all the frames:
nzp = []
for fr in range(im.n_frames):
im.seek(fr)
nzpf = []
for i in range(im.height):
for j in range(im.width):
p = im.getpixel((j, i))
if p != 0 and p != (0, 0, 0):
nzpf.append((i, j, p))
nzp.append(nzpf)
nzp[:50]
[[(100, 100, 8)], [(102, 100, (8, 8, 8))], [(102, 100, (8, 8, 8))], [(102, 100, (8, 8, 8))], [(102, 100, (8, 8, 8))], [(102, 100, (8, 8, 8))], [(102, 100, (8, 8, 8))], [(102, 100, (8, 8, 8))], [(102, 100, (8, 8, 8))], [(102, 102, (8, 8, 8))], [(100, 102, (8, 8, 8))], [(100, 102, (8, 8, 8))], [(100, 102, (8, 8, 8))], [(100, 102, (8, 8, 8))], [(100, 102, (8, 8, 8))], [(100, 102, (8, 8, 8))], [(100, 102, (8, 8, 8))], [(98, 102, (8, 8, 8))], [(98, 100, (8, 8, 8))], [(98, 100, (8, 8, 8))], [(98, 100, (8, 8, 8))], [(98, 98, (8, 8, 8))], [(100, 98, (8, 8, 8))], [(100, 98, (8, 8, 8))], [(100, 98, (8, 8, 8))], [(100, 98, (8, 8, 8))], [(100, 98, (8, 8, 8))], [(100, 98, (8, 8, 8))], [(102, 98, (8, 8, 8))], [(100, 100, (8, 8, 8))], [(98, 98, (8, 8, 8))], [(98, 98, (8, 8, 8))], [(100, 98, (8, 8, 8))], [(100, 98, (8, 8, 8))], [(100, 98, (8, 8, 8))], [(100, 98, (8, 8, 8))], [(100, 98, (8, 8, 8))], [(102, 98, (8, 8, 8))], [(102, 98, (8, 8, 8))], [(102, 98, (8, 8, 8))], [(102, 100, (8, 8, 8))], [(102, 100, (8, 8, 8))], [(102, 102, (8, 8, 8))], [(102, 102, (8, 8, 8))], [(102, 102, (8, 8, 8))], [(100, 102, (8, 8, 8))], [(100, 102, (8, 8, 8))], [(100, 102, (8, 8, 8))], [(100, 102, (8, 8, 8))], [(100, 102, (8, 8, 8))]]
And for sure, precisely one pixel from each frame is nonzero! The problem becomes: now… what?
I guess let’s first make these pixels more visible to the human eye:
import IPython.display
frames = []
for fr in range(im.n_frames):
im.seek(fr)
j, i, _ = nzp[fr][0]
p = im.getpixel((j, i))
if isinstance(p, int):
im.putpixel((j, i), im.palette.colors[255, 255, 255])
else:
im.putpixel((j, i), (255, 255, 255))
f = io.BytesIO()
im.save(f, format='gif', save_all=False)
f.seek(0)
frames.append(Image.open(f))
f = io.BytesIO()
frames[0].save(f, save_all=True, format='gif', append_images=frames[1:])
IPython.display.Image(f.getvalue())
That is very cool! But what does it mean? Thinking back to the problem, it says “emulate” with a joystick. Ah OK, I think I understand!
The white pixel is moving in the 9 pixels forming a 3×3 square, and it represents a joystick. The middle (100, 100) means “don’t move”, and the 8 pixels surrounding it means “move 1px in this direction”. So if we emulate the movement of the pixel based on this GIF, we should get something!
Let’s do it!
newframes = []
last = {}
j, i = 100, 100
for fr in range(im.n_frames):
x, y, _ = nzp[fr][0]
j += (y - 100) // 2
i += (x - 100) // 2
newim = Image.new('RGB', im.size)
newim.putpixel((j, i), (255, 255, 255))
newframes.append(newim)
f = io.BytesIO()
newframes[0].save(f, save_all=True, format='gif', append_images=newframes[1:])
IPython.display.Image(f.getvalue())
It looks like it’s tracing something… But what?
last = {}
j, i = 100, 100
newim = Image.new('RGB', im.size)
for fr in range(im.n_frames):
x, y, _ = nzp[fr][0]
j += (y - 100) // 2
i += (x - 100) // 2
newim.putpixel((j, i), (255, 255, 255))
newim
That looks like garbage…
Inspecting the data again, it seems that (100, 100) (which in our interpretation means “don’t move”) only appears 5 times. Maybe… We should split at each time that appears?
last = {}
def draw(start) -> None:
j, i = 100, 100
newim = Image.new('RGB', im.size)
for fr in range(start, im.n_frames):
x, y, _ = nzp[fr][0]
if (x, y) == (100, 100):
break
j += (y - 100) // 2
i += (x - 100) // 2
newim.putpixel((j, i), (255, 255, 255))
return newim
draw(1)
Is that… a letter “b”?
for fr in range(im.n_frames):
x, y, _ = nzp[fr][0]
if (x, y) == (100, 100):
display(draw(fr + 1))
It says “bonus”, annnnnd we’re done!
Level 23¶
http://butter:fly@www.pythonchallenge.com/pc/hex/bonus.html
Title: what is this module?
In the source:
<!--
TODO: do you owe someone an apology? now it is a good time to
tell him that you are sorry. Please show good manners although
it has nothing to do with this level.
-->
<!-- it can't find it. this is an undocumented module. -->
<!--
'va gur snpr bs jung?'
-->
Well, ain’t this an easy level.
The “this module” refers to the module called, well, this
. Open a Python terminal and type import this
, and you will get as output:
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
Furthermore, the this
module is undocumented, so you can’t find it in the docs!
Looking at the source code of this
, it appears to use a rot13
algorithm to encode the text, which just means each letter is shifted by 13 letters. So,
abcdefghijklmnopqrstuvwxyz
becomes
nopqrstuvwxyzabcdefghijklm
This is a trivial encoding method, but it appears to be what the text in this problem uses too!
import codecs
codecs.decode('va gur snpr bs jung?', 'rot13')
'in the face of what?'
Back to The Zen of Python:
In the face of ambiguity, refuse the temptation to guess.
And the problem is solved!
Level 24¶
http://butter:fly@www.pythonchallenge.com/pc/hex/ambiguity.html
Title: from top to bottom
Nothing in the source.
The image is obviously a maze, which the filename generously tells us. Inspecting the image closer, there is one black pixel on the top row and one on the bottom row. White seems to be the “walls” of the maze, and black/red is the path.
Let’s walk the maze then!
import io
import requests
from PIL import Image
r = requests.get('http://www.pythonchallenge.com/pc/hex/maze.png', auth=auth)
im = Image.open(io.BytesIO(r.content))
repr(im)
'<PIL.PngImagePlugin.PngImageFile image mode=RGBA size=641x641 at 0x7FBE880C6510>'
# px[row][column] is the pixel at (row, column)
px = []
w, h = im.size
for i in range(h):
row = []
for j in range(w):
row.append(im.getpixel((j,i)))
px.append(row)
import sys
sys.setrecursionlimit(10000000)
# traverse the maze with DFS!
vis = set()
path = []
def dfs(i, j):
if (
i < 0
or j < 0
or i >= h
or j >= w
or (i, j) in vis
or px[i][j] == (255, 255, 255, 255)
or px[i][j] == (127, 127, 127, 255)
):
return False
vis.add((i, j))
path.append((i, j))
if i == h - 1:
return True
if dfs(i + 1, j) or dfs(i, j - 1) or dfs(i - 1, j) or dfs(i, j + 1):
return True
vis.remove((i, j))
path.pop()
return False
assert dfs(0, w - 2)
Perfect! Let’s draw the path:
newim = Image.new('RGB', im.size)
for i, j in path:
newim.putpixel((j, i), (255, 255, 255))
newim
Wow… wow. That’s a long path! But the path tells us nothing… I thought it might display some text or something…
I suppose the red pixels must mean something. What are the pixels along this path?
ppx = []
for i,j in path:
ppx.append(px[i][j])
ppx[:20]
[(0, 0, 0, 255), (80, 0, 0, 255), (0, 0, 0, 255), (75, 0, 0, 255), (0, 0, 0, 255), (3, 0, 0, 255), (0, 0, 0, 255), (4, 0, 0, 255), (0, 0, 0, 255), (20, 0, 0, 255), (0, 0, 0, 255), (0, 0, 0, 255), (0, 0, 0, 255), (0, 0, 0, 255), (0, 0, 0, 255), (0, 0, 0, 255), (0, 0, 0, 255), (8, 0, 0, 255), (0, 0, 0, 255), (0, 0, 0, 255)]
So it appears that only the R color changes, G and B stay 0, and A stays 255. And the R’s seem to alternate between 0 and a number… Maybe it signifies a byte?
bs = b''
for i in range(1, len(ppx), 2):
bs += bytes([ppx[i][0]])
bs[:50]
b'PK\x03\x04\x14\x00\x00\x00\x08\x00\x88\x9a\xb02\xa5\xb9\xd9\xb2\xe7G\x00\x00\xa0L\x00\x00\x08\x00\x15\x00maze.jpgUT\t\x00\x03?\xc8\x88B\xfa\xd1\x8a'
Well if that ain’t another ZIP file!
import zipfile
zf = zipfile.ZipFile(io.BytesIO(bs))
zf.infolist()
[<ZipInfo filename='maze.jpg' compress_type=deflate filemode='-rw-r--r--' file_size=19616 compress_size=18407>, <ZipInfo filename='mybroken.zip' filemode='-rw-r--r--' file_size=2701>]
So there’s another maze and a “mybroken.zip” file inside… Let’s look at the maze.jpg first:
with zf.open('maze.jpg') as f:
im = Image.open(io.BytesIO(f.read()))
im.convert('RGBA')
“lake”, it says – and we’re done!
Level 25¶
Title: imagine how they sound
In the source:
<!-- can you see the waves? -->
Waves & sound point to .WAV files, and the image filename is “lake1.jpg”. I tried “lake2.jpg” to no avail, but “lake1.wav” is indeed a file! So is “lake2.wav”, all the way until “lake25.wav”. All of them sound like random noise. Connecting that with the image of a jigsaw puzzle, I know that I need to combine these files somehow to form an actual audio file. But, how?
import requests
import io
import wave
wvs = []
for i in range(1, 26):
r = requests.get(f'http://www.pythonchallenge.com/pc/hex/lake{i}.wav', auth=auth)
f = wave.open(io.BytesIO(r.content), 'r')
wvs.append(f)
# example audio
IPython.display.Audio(r.content)
Inspecting the audio files…
wv = wvs[0]
wv.getnframes(), wv.getframerate()
(10800, 9600)
wv.readframes(100)
b'\xff\xff\xff\xfe\xfe\xfe\xfe\xfe\xfe\xfe\xfe\xfe\xff\xff\xff\xfe\xfe\xfe\xfe\xfe\xfe\xfe\xfe\xff\xfd\xfd\xff\xfc\xfc\xfe\xfd\xfc\xff\xfc\xfb\xff\xfb\xfb\xff\xfb\xfb\xff\xfc\xfb\xff\xfd\xfc\xff\xfe\xfe\xff\xfe\xfe\xff\xfd\xfd\xff\xfb\xfc\xff\xf9\xfb\xff\xf8\xfb\xff\xf8\xfb\xff\xf8\xfc\xff\xf8\xfc\xff\xf9\xfb\xff\xfa\xfc\xff\xfb\xfc\xff\xf9\xfb\xff\xf7\xfb\xff\xf6\xfb\xff\xf6\xfa\xff\xf4\xf8\xff\xf5'
Hmm… The frames seem to be almost all 0xff’s, which explains the noise.
I have a bold idea: what if the wave files are actually images, RGB encoded? The 10800 frames in each wave file is precisely 60x60x3, which means each wave file could be a 60×60 image. Then putting these images together would yield a big image!
Let’s test it:
im = Image.new('RGB', (300,300))
for i in range(25):
bx,by = i%5*60, i//5*60
wv = wvs[i]
wv.setpos(0)
for y in range(60):
for x in range(60):
r,g,b = wv.readframes(3)
im.putpixel((bx+x,by+y), (r,g,b))
im
Decent indeed!
OK I think that’s enough for one post. See you again later!