Legends ofPythos
Claim your name
The Standard Library

Jörmungandr, the World Serpent

The last fight of this track

Boss · level 18

Jörmungandr

the World Serpent

Health5 of 5 stages standing
Jörmungandr, the World Serpent, lies coiled around the whole of Midgard with his tail in his mouth. When he stirs, the fjords rise. The watchers of the coast keep a log of every coil he moves, and the jarls pay tribute to keep him sleeping. This is the last fight on the road, and it asks for everything you have learned: read the log, reckon the tribute to the penny, report it cleanly, keep the records, and bind the serpent itself.

The fight

Five stages, each building on the last. Every stage opens with your finished code from the stage before, so you pick up where you left off. This fight draws on everything: regular expressions, Decimal, format specifications, pathlib, json, logging, classes, generators and match.
Stage 1: The watchers' log. Read the log, and throw out the lines that went wrong.
Stage 2: The tribute. Reckon every fjord's tribute to the penny.
Stage 3: The report. Set the tribute out in a table the jarls can read.
Stage 4: The records. Keep the records safe on disk, and log that you did.
Stage 5: The binding. Bind the World Serpent himself.

The fight

0 of 5 stages won

Stage 1 of 5

+80 XP
The coast watchers keep LOG, one sighting per line in the form 2026-09-01 04:12 COIL depth=340 fjord=Hardanger, though two lines went wrong. Write parse(line), which uses re.fullmatch with groups to return a dictionary with the keys 'date', 'time', 'depth' and 'fjord' (the depth as an int), or None when the line does not match. Then write sightings(text), which returns the parsed dictionaries for every good line of text, in order. For example, parse('2026-09-05 01:40 COIL depth=260 fjord=Geiranger') is {'date': '2026-09-05', 'time': '01:40', 'depth': 260, 'fjord': 'Geiranger'}, and len(sightings(LOG)) is 7.
import re


LOG = """2026-09-01 04:12 COIL depth=340 fjord=Hardanger
2026-09-01 22:47 COIL depth=515 fjord=Sogne
2026-09-02 03:05 COIL depth=120 fjord=Geiranger
the watchman fell asleep here
2026-09-03 11:30 COIL depth=880 fjord=Sogne
2026-09-02 19:58 COIL depth=410 fjord=Hardanger
2026-09-04 06:15 COIL depth=95 fjord=Lyse
2026-09-04 07:20 COIL depth=deep fjord=Lyse
2026-09-05 01:40 COIL depth=260 fjord=Geiranger"""




def parse(line):
    # re.fullmatch with groups; a dictionary for a good line, None otherwise
    pass




def sightings(text):
    # The parsed dictionaries for every good line, in order
    pass

Run your code to check it against the tests.

Stage 2 of 5

+80 XP
The jarls pay tribute by the metre. RATES maps each fjord to its rate per metre of depth, as a Decimal. Keep stage 1 and write tribute(seen, rates), where seen is a list of sightings; it returns a dictionary of fjord to tribute. For each fjord, add up depth times rate over its sightings, then round the total to pennies with quantize(Decimal('0.01')). A fjord with no rate pays Decimal('0.00'). For example, the two Hardanger sightings in LOG come to 750 metres, so Hardanger pays Decimal('26.25').
import re


LOG = """2026-09-01 04:12 COIL depth=340 fjord=Hardanger
2026-09-01 22:47 COIL depth=515 fjord=Sogne
2026-09-02 03:05 COIL depth=120 fjord=Geiranger
the watchman fell asleep here
2026-09-03 11:30 COIL depth=880 fjord=Sogne
2026-09-02 19:58 COIL depth=410 fjord=Hardanger
2026-09-04 06:15 COIL depth=95 fjord=Lyse
2026-09-04 07:20 COIL depth=deep fjord=Lyse
2026-09-05 01:40 COIL depth=260 fjord=Geiranger"""




def parse(line):
    m = re.fullmatch(r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}) COIL depth=(\d+) fjord=(\w+)', line)
    if m is None:
        return None
    date, time, depth, fjord = m.groups()
    return {'date': date, 'time': time, 'depth': int(depth), 'fjord': fjord}




def sightings(text):
    found = []
    for line in text.splitlines():
        sighting = parse(line)
        if sighting is not None:
            found.append(sighting)
    return found




from decimal import Decimal


RATES = {
    'Hardanger': Decimal('0.035'),
    'Sogne': Decimal('0.0425'),
    'Geiranger': Decimal('0.05'),

Run your code to check it against the tests.

Stage 3 of 5

+80 XP
Keep everything and write report(dues), which takes a dictionary like the one tribute returns and gives back a table as one string, its lines joined with newlines. The first line is f"{'Fjord':<12}{'Tribute':>12}". Then comes one line per fjord, largest tribute first (ties in name order), formatted f'{name:<12}{amount:>12,.2f}'. The last line is f"{'Total':<12}{total:>12,.2f}". For example, the second line of report({'Sogne': Decimal('1234.5')}) is 'Sogne 1,234.50'.
import re


LOG = """2026-09-01 04:12 COIL depth=340 fjord=Hardanger
2026-09-01 22:47 COIL depth=515 fjord=Sogne
2026-09-02 03:05 COIL depth=120 fjord=Geiranger
the watchman fell asleep here
2026-09-03 11:30 COIL depth=880 fjord=Sogne
2026-09-02 19:58 COIL depth=410 fjord=Hardanger
2026-09-04 06:15 COIL depth=95 fjord=Lyse
2026-09-04 07:20 COIL depth=deep fjord=Lyse
2026-09-05 01:40 COIL depth=260 fjord=Geiranger"""




def parse(line):
    m = re.fullmatch(r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}) COIL depth=(\d+) fjord=(\w+)', line)
    if m is None:
        return None
    date, time, depth, fjord = m.groups()
    return {'date': date, 'time': time, 'depth': int(depth), 'fjord': fjord}




def sightings(text):
    found = []
    for line in text.splitlines():
        sighting = parse(line)
        if sighting is not None:
            found.append(sighting)
    return found




from decimal import Decimal


RATES = {
    'Hardanger': Decimal('0.035'),
    'Sogne': Decimal('0.0425'),
    'Geiranger': Decimal('0.05'),

Run your code to check it against the tests.

Stage 4 of 5

+80 XP
The watch keeps its records on disk. Keep everything and write keep(dues, folder), which creates folder with pathlib (along with any missing parent folders, and without failing if it is already there), writes dues to tribute.json inside it with json.dump, with the amounts written as strings, and logs f'kept {n} fjords' at INFO level on the watch logger that is set up for you, where n is the number of fjords. Then write recall(folder), which reads the file back with json.load and returns the dictionary with Decimal amounts again. The last line of the program calls keep once, so the log shows in the output as INFO:watch:kept 4 fjords.
import re


LOG = """2026-09-01 04:12 COIL depth=340 fjord=Hardanger
2026-09-01 22:47 COIL depth=515 fjord=Sogne
2026-09-02 03:05 COIL depth=120 fjord=Geiranger
the watchman fell asleep here
2026-09-03 11:30 COIL depth=880 fjord=Sogne
2026-09-02 19:58 COIL depth=410 fjord=Hardanger
2026-09-04 06:15 COIL depth=95 fjord=Lyse
2026-09-04 07:20 COIL depth=deep fjord=Lyse
2026-09-05 01:40 COIL depth=260 fjord=Geiranger"""




def parse(line):
    m = re.fullmatch(r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}) COIL depth=(\d+) fjord=(\w+)', line)
    if m is None:
        return None
    date, time, depth, fjord = m.groups()
    return {'date': date, 'time': time, 'depth': int(depth), 'fjord': fjord}




def sightings(text):
    found = []
    for line in text.splitlines():
        sighting = parse(line)
        if sighting is not None:
            found.append(sighting)
    return found




from decimal import Decimal


RATES = {
    'Hardanger': Decimal('0.035'),
    'Sogne': Decimal('0.0425'),
    'Geiranger': Decimal('0.05'),

Run your code to check it against the tests.

Stage 5 of 5

+80 XP
The last binding. Keep everything and write an exception class Unseen and a class Serpent that takes a list of sightings. Looping over a serpent yields its sightings in date-and-time order, from a generator in __iter__. deepest() returns the sighting with the greatest depth, or None when there are none. command(line) uses match on the words of the line: 'sleep' returns 'The serpent sleeps.'; 'stir <fjord>' returns f'The serpent stirs in {fjord}.' if that fjord appears in the sightings, and raises Unseen if it does not; anything else returns 'The serpent does not answer.'
import re


LOG = """2026-09-01 04:12 COIL depth=340 fjord=Hardanger
2026-09-01 22:47 COIL depth=515 fjord=Sogne
2026-09-02 03:05 COIL depth=120 fjord=Geiranger
the watchman fell asleep here
2026-09-03 11:30 COIL depth=880 fjord=Sogne
2026-09-02 19:58 COIL depth=410 fjord=Hardanger
2026-09-04 06:15 COIL depth=95 fjord=Lyse
2026-09-04 07:20 COIL depth=deep fjord=Lyse
2026-09-05 01:40 COIL depth=260 fjord=Geiranger"""




def parse(line):
    m = re.fullmatch(r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}) COIL depth=(\d+) fjord=(\w+)', line)
    if m is None:
        return None
    date, time, depth, fjord = m.groups()
    return {'date': date, 'time': time, 'depth': int(depth), 'fjord': fjord}




def sightings(text):
    found = []
    for line in text.splitlines():
        sighting = parse(line)
        if sighting is not None:
            found.append(sighting)
    return found




from decimal import Decimal


RATES = {
    'Hardanger': Decimal('0.035'),
    'Sogne': Decimal('0.0425'),
    'Geiranger': Decimal('0.05'),

Run your code to check it against the tests.