SANS 2019 Holiday Hack Frosty Keypad Challenge

I had some time to check out the SANS Holiday Hack this year and ran into a fun challenge involving guessing the pin to a keypad. This post walks through how I solved this challenge by writing a Python script to help determine the pin.

One of the elves provided some hints to help you determine the pin: One digit is repeated once, it’s prime, and you can see which keys were used. The keypad looked like this:

This leads us to believe that the keys used are 1, 3, and 7. If I would have read the hints carefully, I may have realized that I didn’t need to write a script for this because the pin can only be four digits in length (since only one digit is repeated once), but writing the script ended up being fun for me so here’s what I did:

Determining Prime Numbers

The hint mentions that the pin is a prime number, and I also know the pin needs to only have the numbers 1, 3, and 7. I put this code at the beginning of my main function:


def main():

    # minimum and maximum values to check
    start = 1000
    end = 9999999
    
    # Creats an empty list to store prime numbers
    prime_list = []
    for val in range(start, end): 
        
        # We only want numbers that include 1, 3, and 7
        if '1' in str(val) and '3' in str(val) and '7' in str(val):
            
            # We don't want any other number
            bad_nums = set('0245689')
            if any((b in bad_nums) for b in str(val)):
                continue

            # If num is divisible by any number   
            # between 2 and val, it is not prime  
            if val > 1: 
                for n in range(2, val): 
                    if (val % n) == 0: 
                        break
                else: 
                    prime_list.append(val)

This leaves me with a list of prime numbers that only contain the numbers 1, 3, and 7 stored in the prime_list variable. As mentioned, I should have noticed that the pin had to be four digits, but since I didn’t, I was calculating prime numbers up to seven digits long (it starts taking a really long time the more digits you try). Once I had my list of prime numbers, I needed a way to check if there were any duplicate digits. This was easier said than done, because while it was okay to have a number like 1337, it was not okay to have 31337, or 11337, since only a single digit repeats once.

Finding a Single, Repeating Digit

To do this, I wrote a function called has_single_double, that took in a number and returned that number only if it contained a single, repeating digit. Here is the code:


def has_single_double(n):
    """Returns a number that has one (and only one) 
    repeating digit.
    """
    s = str(n)
    count_obj = Counter(s)
    
    # Counter to ensure only 1 digit repeats
    dbl_counter = []

    # Iterate through the counter object dict
    for key in count_obj:
        k = key
        val = count_obj[key]

        # If a digit repeats more than two times, return None
        if val > 2:
            return 

        # Ensures that only one digit repeats by adding it to the counter
        if val == 2:
            dbl_counter.append(val)
    
    # Returns only if a single digit is repeated once.
    if len(dbl_counter) == 1:
        return(n)

Let me explain this function and its use of the Counter(). Counter is from the collections module and here is what the documentation says about it:

A Counter is a dict subclass for counting hashable objects. It is an unordered collection where elements are stored as dictionary keys and their counts are stored as dictionary values. Counts are allowed to be any integer value including zero or negative counts. The Counter class is similar to bags or multisets in other languages. 

Here is how you can experiment with it in a terminal:

>>> from collections import Counter
>>> n = 31337
>>> s = str(n)
>>> count_obj = Counter(s)
>>> type(count_obj)
<class 'collections.Counter'>
>>> count_obj
Counter({'3': 3, '1': 1, '7': 1})
>>> dir(count_obj)
['__add__'...'items', 'keys', 'most_common', 'pop', 'popitem', 'setdefault', 'subtract', 'update', 'values']
>>> count_obj.keys()
dict_keys(['3', '1', '7'])
>>> count_obj.values()
dict_values([3, 1, 1])
>>> for key in count_obj: print(key, count_obj.get(key))
...
3 3
1 1
7 1
>>>

So when passing the ‘31337’ to Counter, it returns a dictionary object where the key is the character, and value is the number of times that character is counted. The other code that I included in the function helps ensure that only one number is repeated only once.

This function was called within a loop right after I got my list of of prime numbers:


                for n in range(2, val): 
                    if (val % n) == 0: 
                        break
                else: 
                    prime_list.append(val)
    # Checks for repeating digits
    for num in prime_list:
        value = has_single_double(num)
        if value:
            print(value)

This results in only a few pin possibilities, one of which is the correct answer.

I definitely overcomplicated this challenge, but this was a fun bit of code to write, and Counter is a good thing to know. Here’s the full script:


from collections import Counter

def has_single_double(n):
    """Returns a number that has one (and only one) 
    repeating digit.
    """
    s = str(n)
    count_obj = Counter(s)
    
    # Counter to ensure only 1 digit repeats
    dbl_counter = []

    # Iterate through the counter object dict
    for key in count_obj:
        k = key
        val = count_obj[key]

        # If a digit repeats more than two times, return None
        if val > 2:
            return 

        # Ensures that only one digit repeats by adding it to the counter
        if val == 2:
            dbl_counter.append(val)
    
    # Returns only if a single digit is repeated once.
    if len(dbl_counter) == 1:
        return(n)

def main():

    # minimum and maximum values to check
    start = 1000
    end = 99999
    
    # Creats an empty list to store prime numbers
    prime_list = []
    for val in range(start, end): 
        
        # We only want numbers that include 1, 3, and 7
        if '1' in str(val) and '3' in str(val) and '7' in str(val):
            
            # We don't want any other number
            bad_nums = set('0245689')
            if any((b in bad_nums) for b in str(val)):
                continue

            # If num is divisible by any number   
            # between 2 and val, it is not prime  
            if val > 1: 
                for n in range(2, val): 
                    if (val % n) == 0: 
                        break
                else: 
                    prime_list.append(val)

    # Checks for repeating digits
    for num in prime_list:
        value = has_single_double(num)
        if value:
            print(value)

if __name__ == '__main__':
    main()

 

Leave a Reply

Your email address will not be published. Required fields are marked *