7 minute read

A couple of days ago my girlfriend told me about this quiz she found - the python challenge. Its basically a web quiz containing levels and you progress levels by solving puzzles. If you’ve ever heard of NotPron, you know what I’m talking about. The puzzles on the python challenge are designed in a way so that you have to utilize some algorithm or similar to solve them and I thought that might be a fun way to learn something new. I’ll detail how I solved each level and which problems I faced in doing so.If you plan on doing the challenge yourself, you might now want to continue reading.

Level 0

A picture of a number, and the hint to change the URL. Simple,right? Well, no. Actually its 2 to the power of 38. In python we can calculate that number using math.pow(2, 38).

result = pow(2, 38)
print(result)

Still easy enough, could have done that without python.

Level 1

A picture of some text with arrows. Also more garbled text. Probably means that we should shift the letters by a certain number, which remind me of the Caesar cipher. So we simply have to calculate the distance of the given letters (using ASCII codes) and apply that shift to the message.

def decode(shift, text):
    output = []
    for c in text:
        if c.isalpha():
            newOrd = ord(c) + shift
            if newOrd > 122:
                newOrd -= 26
            output.append(chr(newOrd))
        else:
            output.append(c)
    return "".join(output)

text = "g fmnc wms bgblr rpylqjyrc gr zw fylb. rfyrq ufyr amknsrcpq ypc dmp. bmgle gr gl zw fylb gq glcddgagclr ylb " \
        "rfyr'q ufw rfgq rcvr gq qm jmle. sqgle qrpgle.kyicrpylq() gq pcamkkclbcb. lmu ynnjw ml rfc spj."
shift = ord('M') - ord('K')
print(decode(shift, text))

The first result is still a bit garbled, which is because we didn’t consider that shifting the last letters of the alphabet results in the ASCII-Codes of signs like ‘{’. So we add the case where newOrd > 122. Now we get the proper message, which tells us to apply the shift to the URL.

url = "map"
print(decode(shift, url))

Level 2

Well, this is a bit misleading. The title of the page is ‘OCR’, but luckily the hint tells us to actually look in the page’s source. There we find a ton of text in the comments and the note to find all rare characters. So let’s add any new character to a dictionary as key and increment the value of that entry by 1 if we find that character again. Then we only have to find those entries with minimal occurrences. But watch out: If we try to do that using a ‘normal’ dictionary we get only nonsense. Better make sure that the entries remain in the order in which they are added using OrderdDict.

from collections import OrderedDict

file = open("3_input.txt", 'r')
text = file.read()
dictionary = OrderedDict()
for c in text:
    indict = c in dictionary
    if indict:
        dictionary[c] += 1
    else:
        dictionary[c] = 1
print(dictionary)
filtered = []
for k in dictionary.keys():
    if dictionary[k] == min(dictionary.values()):
        filtered.append(k)
print("".join(filtered))

Level 3

The hint tells us that we should look for a small letter surrounded by exactly 3 bodyguards. Looking at the source code reveals a bunch of text - again. I was spoiled at this point, because I already knew that ‘Bodyguards’ meant upper-case letters. The resulting code is not exactly efficient, but it does the trick.

file = open("4_input.txt", 'r')
text = file.read()

def checkBodyguards(text, index):
    return (text[index-4].islower()
        and text[index-3].istitle()
        and text[index-2].istitle()
        and text[index-1].istitle()
        and text[index+1].istitle()
        and text[index+2].istitle()
        and text[index+3].istitle()
        and text[index+4].islower())

for index in range(3, len(text)):
    if text[index].isalpha() and not text[index].istitle():
        if checkBodyguards(text, index):
            print(text[index])

Level 4

Okay, this is new. There is no significant source code, and the text just says ‘linkedlist.php’. Huh, probably should change the file extension to ‘.php’. Comments on that page tell us to follow the chain and we see that there is a link to another page. And that page contains the instruction to go to another number. Probably should not try this per hand. After researching how to do requests using urllib2 we can parse the number from the page content, modify the URL for the next request and do that all over again.

import urllib.request as request

def step(number):
    url = base + str(number)
    page = request.urlopen(url).read()
    number = [int(s) for s in page.split() if s.isdigit()]
    return number[0]

def crawl(start):
    next = start
    while True:
        try:
            next = step(next)
        except:
            print("DONE")

base = "http://www.pythonchallenge.com/pc/def/linkedlist.php?nothing="
crawl(45439)

Now, after the site refused to load a couple of times the code actually runs for some time - until it fails. Let’s figure out why. If we print out the page content we see that the last page actually contains an instruction to divide the last number by 2. After we adjust the function step to handle that case we can continue.

...
old = number
if not number:
    return int(old / 2)
print(number[0])
...

Level 5

This one was mean. Again, I was spoiled because my girlfriend had asked me minutes before if there was such a thing as ‘pickle’ in Python. Well, I had no clue, but google tells us that, yes, there is such a thing. pickling appears to be some sort of object serialization. The source code of the page contains a lot of weird text, so our goal should be to unpickle that text. The code is pretty straight forward.

import pickle

file = open('6.p', 'rb');
array = pickle.load(file)

Unpickling gives us… a list with more stuff. Great. As there appear to be only hash-tags and empty spaces in that list we can conclude that we should probably arrange its contents into some sort of ASCII-Art. The sum of the digits in the list always equals 95, so that seems to be the width of the result. Printing each character the number of times specified gives us the solution.

banner = ""
for row in array:
    rowContent = ""
    for item in row:
        rowContent += item[0] * item[1]
    print(rowContent)

Level 6

I remember this one, because at this point I knew to much for my own good. I was already familiar with the concept of zip so I asked myself: “What, I can zip not only lists but also text? What does that do?” Well, don’t you try, because it is utterly wrong. The solution is simple: Change the ‘.html’ in the URL to ‘.zip’.
The zip file we receive contains a lot of files with numbers as name and a read-me file. So, we do the same thing we just did but with text files. Not that exciting. The final text file tells us to collect the comments. Which comments? Luckily we already found information how to extract file info from zip files and change our code accordingly.

import zipfile

def get_next(content):
    number = [int(s) for s in content.split() if s.isdigit()]
    return number[0]

def crawl(zf):
    extension = '.txt'
    next_number = 90052
    comment = ""
    while True:
        try:
            name = str(next_number) + extension
            content = zf.read(name)
            comment += zf.getinfo(name).comment.decode('utf-8')
            next_number = get_next(content)
        except IndexError:
            print("The next step is:" + str(content))
            break
    print("Comments collected: "+ comment)

if __name__ == "__main__":
    zf = zipfile.ZipFile('7.zip', 'r')
    crawl(zf)

We get one answer, change the URL accordingly and are subsequently stumped what the hell THAT is supposed to mean. Luckily the stumping doesn’t last long, and we conclude that we have to look at the letters the previous answer was made of. In retrospect, that was obvious.

Level 7

This level contains only an image with some grey scale pixels. Probably we should have a look at the RGB-Values of the grey squares. Hmm, the range of those values looks familiar. Could it be… ASCII? Again? Yeah, sure it is. So basically we convert the grey squares to characters and get the solution.

from PIL import Image

img = Image.open('8.png').convert('RGB')
width, height = img.size

characters = []
for i in range(1, 607, 7):
    r, g, b = img.getpixel((i, height/2))
    characters.append(chr(r))

print("".join(characters))
solution = [105, 110, 116, 101, 103, 114, 105, 116, 121]
print("".join([chr(x) for x in solution]))

Level 8

The source code of the level reveals that there is link to clock on. Also some encoded user name and password. What for? Let’s click on the link and find out. So, it is pretty obvious that we should decode the given data. But which encoding. After some unsuccessful attempts using zlib we conclude that we did NOT try the right thing. Some more googling shows that we might want to try using bz2.

from bz2 import decompress

file = open('9.txt', 'rb')
content = file.readline()
username = decompress(b'BZh91AY&SYA\xaf\x82\r\x00\x00\x01\x01\x80\x02\xc0\x02\x00 \x00!\x9ah3M\x07<]\xc9\x14\xe1BA\x06\xbe\x084')
password = decompress(b'BZh91AY&SY\x94$|\x0e\x00\x00\x00\x81\x00\x03$ \x00!\x9ah3M\x13<]\xc9\x14\xe1BBP\x91\xf08')
print(username)
print(password)

It seems that did the trick!

Level 9

That should be another easy one. Connect the dots? The stuff in the comments should be coordinates then. Let’s use PIL. The code is very straight forward.

from PIL import Image, ImageDraw

def draw_dots(filename):
    im = Image.new('RGBA', (400, 400), (0, 0, 0, 0))
    draw = ImageDraw.Draw(im)
    coords = open(filename, 'r').read().split(',')
    first = tuple([int(x) for x in coords])
    draw.line(first, fill=255, width=3)
    im.show()

if __name__ == "__main__":
    draw_dots('10_first.txt')
    draw_dots('10_second.txt')

I’ll write about the next few levels in the next part of this series.

Updated: