AoC 2017 Day 13: Firewall Puncher

Source: Packet Scanners

Part 1: Multiple layers are defined with rules of the form:

  • {index}: {depth}

Each layer will start at position 0, then once per tick will advance towards depth. Once it hits depth-1, it will return to position 0, taking 2*depth-1 per full cycle.

Calculate the sum of index * depth for any scanners that are at position 0 when you pass through them given an initial starting time.

The main simplifying assumption you can make is that you don’t actually have to simulate all of the layers. You just have to calculate where it will be when you get there:

def calculate_severity(delay):
    '''
    Calculate how severe the alarm is if you start at the given tick.
    '''

    total_severity = 0

    for depth in firewalls:
        range = firewalls[depth]

        cycle_length = (range - 1) * 2
        position = (delay + depth) % cycle_length

        # This isn't actually necessary, but makes debugging easier
        if position > range:
            position = 2 * range - position

        if position == 0:
            severity = depth * firewalls[depth]
            total_severity += severity

    return total_severity

You can use this to find the severity starting at a given ticket:

tick = lib.param('tick')
severity = calculate_severity(tick)
print(f'{tick}: severity {severity}')

Part 2: Find the first timestamp you can delay to such that you are never at position 0 with the scanner for any layer.

For this, we don’t care about total severity so we can modify that function to bail out early:

def calculate_severity(delay, return_pass_all = False):
    '''
    Calculate how severe the alarm is if you start at the given tick.

    If return_pass_all is set, return True if you hit no walls and False if you
    hit any (even if the severity would be 0).
    '''

    total_severity = 0

    for depth in firewalls:
        range = firewalls[depth]

        cycle_length = (range - 1) * 2
        position = (delay + depth) % cycle_length

        if position > range:
            position = 2 * range - position

        if position == 0:
            severity = depth * firewalls[depth]
            total_severity += severity

            if return_pass_all:
                return False

    if return_pass_all:
        return True
    else:
        return total_severity

Then just iterate until we find a safe tick:

safe_tick = None
for tick in itertools.count():
    lib.log('=== {} ===', tick)
    if calculate_severity(tick, return_pass_all = True):
        safe_tick = tick
        break

print(f'safe: {safe_tick}')

It’s slow, but less than a minute so it works out:

$ python3 run-all.py day-13

day-13  python3 firewall-puncher.py input.txt --tick 0  0.07503604888916016     0: severity 1476
day-13  python3 firewall-puncher.py input.txt --safest  23.190335035324097      safe: 3937334

If I wanted to optimize this, what we’re looking for is the least common multiple of all of the cycle lengths when offset by the depths. That seems like something that should be possible.