Puzzle #4

 9th July 2023 at 11:33am

First part

Another interesting problem.

It naturally occurred to use a split('-') to separate all the contents of a line, and to use a [:-1] on its result to omit the last element (which would be the ID).

not-a-real-room-404[oarel] is now ['not', 'a', 'real'. 'room'].

Since this is a counting exercise, the collections.Counter class is a good candidate. My preliminary code, however, fell a little short:

def custom_ordering(item):
    # Extract the criteria values from the item
    criterion1 = item[0]  # First criterion
    criterion2 = item[1]  # Second criterion

    # Return a tuple containing the criteria values
    return (criterion1, criterion2)

for line in EXAMPLES:
    most_common = Counter([char for word in line.split('-')[:-1] for char in sorted(word)]).most_common(5)
    print(sorted(most_common, key=custom_ordering))
[('a', 5), ('b', 3), ('x', 1), ('y', 1), ('z', 1)]
[('a', 1), ('b', 1), ('c', 1), ('d', 1), ('e', 1)]
[('a', 2), ('n', 1), ('o', 3), ('r', 2), ('t', 1)]
[('a', 2), ('l', 3), ('o', 3), ('r', 2), ('t', 2)]

It will not handle the two sorts at the same time. But there is a small hack one can do: "".join(x.split('-')[:-1]) will get all the contents of a line together (not-a-real-room-404[oarel] is now 'notarealroom'), and can later be wrapped under a sorted() call (thus producing ['a', 'a', 'e', 'l', 'm', 'n', 'o', 'o', 'o', 'r', 'r', 't']). Since the Counter class will index by order of appearance, in the case of same number of occurrences, the earliest in the alphabet will necessarily appear first.

This one liner (Counter([char for word in sorted("".join(line.split('-')[:-1])) for char in word]).most_common(5)) will produce the checksum, which must then be compared.

[('a', 5), ('b', 3), ('x', 1), ('y', 1), ('z', 1)]
[('a', 1), ('b', 1), ('c', 1), ('d', 1), ('e', 1)]
[('o', 3), ('a', 2), ('r', 2), ('e', 1), ('l', 1)]
[('l', 3), ('o', 3), ('a', 2), ('r', 2), ('t', 2)]

I took a break, and came back with some further improvements. With a function to parse the input in the necessary parts, I get

def parse_input_line(line: str):
    content = line.split('-')
    sorted_lowercase = sorted("".join(content[:-1]))
    sector_id = int(content[-1][:-7])
    checksum = content[-1][-6:-1]
    return sorted_lowercase, sector_id, checksum
    

def confirm_checksum_with_most_common(
        sorted_lowercase: str,
        checksum
        ) -> bool:
    return ("".join(list(dict(Counter(sorted_lowercase)
                              .most_common(5)).keys())) == checksum)
        
def iterate_over_input_and_get_count(
        count: int,
        lines: list[str]
        ) -> int:
    if lines:
        sorted_lowercase, sector_id, checksum = parse_input_line(lines[0])
        if confirm_checksum_with_most_common(sorted_lowercase, checksum):
            return iterate_over_input_and_get_count(count + sector_id, lines[1:])
        else:
            return iterate_over_input_and_get_count(count, lines[1:])
    else:
        return count

I'm not very happy with the consecutive transformations from Counter to dict to list, but...it's solved.

Second part

I was struggling to find time for this, but it's also done.

def produce_unencrypted_string(
        original: str,
        sector_id: int
        ) -> str:
       return "".join([lwr[(lwr.index(char) + sector_id) % 26] \
                                       if char != ' ' else ' '
                                       for char in original \
                                       ])
 
def iterate_over_input_and_print_unencrypted(
        lines: list[str]
        ) -> int:
    if lines:
        original, sorted_lowercase, sector_id, checksum = parse_input_line(lines[0])
        if confirm_checksum_with_most_common(sorted_lowercase, checksum) and \
            unencrypted = produce_unencrypted_string(original, sector_id)
            if 'north' in unencrypted:
                print(unencrypted, sector_id)
        return iterate_over_input_and_print_unencrypted(lines[1:])
    else:
        return