15 Aug 2017

How I Used Python To Ruin My Daughter's Summer Vacation

This is the story of how I ruined my 11-year old daughter's summer vacation. Or at any rate, filled it with some algebra, which I am led to believe was not an improvement.

In reality, her summer vacation was going to have some frequent algebra problems in any event, because she was doing ok with the pre-algebra at the end of the school year, but was not doing so well that going three months without any practice was going to be a good idea. So, granted that she is going to have to do some practice problems, how to make this less boring?

The idea I came up with was to turn it into a decoder. So, she gets a message that is encoded such that each letter (or other character such as a space or apostrophe) is replaced with a number. Then, she gets a series of algebra problems to help decode it. Thus, if she solves the problem "8a - 8 = 192" and determines that "a = 25", then everywhere in the puzzle where it has 25, she can replace it with the letter "a". Repeat for a couple dozen algebra problems, and she gets to find out what the puzzle says.

I could generate this by hand, but that would get old, and anyway would require me to do algebra as well, and who wants to do a bunch of stupid algebra all summer long? Not me, so python to the rescue.

First, we have to take the normal input text file, and turn it into one long text string. Nothing much to look at here, except that I did add a warning that if a line was longer than 20 characters, it would be too long once each one was replaced with "____" (and then a number underneath it on the next line). Also, I converted everything to lower case so that "A" and "a" would be replaced by the same number. This probably sets a bad example for my daughter in regards to proper grammar, but it does result in fewer algebra problems she has to solve each day, so I feel she will forgive me.

    
def convert_file_to_text_string(file_path):
    input_file  = open(file_path, 'r')
    text_string = ''
    for new_line in input_file:
        if len(new_line) > 21:
            print('keep your lines short, so we can print them out puzzle-fied!')
            print(new_line)
            sys.exit()
        else:
            text_string += new_line.lower().strip()
            text_string += '\n'
    input_file.close()
    return text_string
    
    

Next, I wanted to have a dictionary of how many times each character occurs. That way, I could make the more frequently occurring letters have the bigger numbers, and vice versa, so that she doesn't solve one of the harder problems and then discover that all that got her was a letter like "q" that only occurs once in the puzzle.

    
def count_char_frequencies(text_string):
    freqs = {}
    for char in text_string:
        if char not in freqs:
            freqs[char] = 1
        else:
            freqs[char] += 1
    return freqs
    
    

Now, I need to create the actual puzzle output, that will look something like this:


___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 19  20  9   26  27  10  7   19  20  27  25  27  24  22  12  26  5
    

For this, we take as input the text string which will be the solution, and the "encoder" which says things like a = 15, b = 4, c = 27, etc. It makes the line of underscores (four for each character so that Juliet doesn't have to write in 8-point font), and then underneath that the number from the decoder. Nothing too hard, the only wrinkle being to check if it's a one-digit or two-digit number so that you can make the line of underscores and the line of numbers line up properly.

    
def create_puzzle_output(text_string,encoder):
    new_text_string = ''
    line_of_blanks  = ''
    line_of_cipher  = ''
    for char in text_string:
        if (char == '\n'):
            new_text_string += char                #empty line to give more vertical space
            new_text_string += line_of_blanks[:-1] #chop off last space
            new_text_string += char
            new_text_string += line_of_cipher[:-1] #chop off last space
            new_text_string += char
            line_of_blanks = ''
            line_of_cipher = ''
        else:
            line_of_blanks += '___ '
            clue = ' ' + str(encoder[char])
            while (len(clue) < len('___ ')):
                clue += ' '
            line_of_cipher += clue
    return new_text_string

    
    

Now, we need to make an algebra problem for every character that we use. I am passing in the number that should be the answer, but other than that I want it to randomly generate some (not too large) numbers for the first coefficient and the first constant. I am also keeping it to integer numbers, at least for June. Maybe in July we'll make things a little more complex, with decimal coefficients. It is summer, after all.

    
def generate_components_one_variable(answer):
    assert(type(answer) is int) #not strictly necessary but I like to check input types
    first_coefficient = determine_sign(random.randrange(1,9,1))
    first_constant    = determine_sign(random.randrange(1,9,1))
    second_constant   = (first_coefficient * answer) + first_constant
    return (first_coefficient,first_constant,second_constant)
    
    

Turn that into a string of text. I should probably tweak this to not put a plus sign in front of a negative first constant, but it is legible enough. I do also use a description in brackets instead of the character if it is a punctuation mark, because otherwise it was confusing to read.

    
def generate_problem_text_string(coeff1,const1,const2,letter):
    if letter == '_':
        printable_letter = '[space]'
    elif letter == '\'':
        printable_letter = '[apostrophe]'
    elif letter == ',':
        printable_letter = '[comma]'
    elif letter == '.':
        printable_letter = '[period]'
    else:
        printable_letter = letter
    pts = '{0}x{1} + {2} = {3}    {1} = ___?'.format(coeff1,printable_letter,const1,const2)
    return pts
    
    

So, put it all together (along with some glue logic to call the appropriate functions at the right times, and load the text input file and write the files for the puzzle and worksheet), and you can turn this:

Once upon a time, 
there was a little 
girl named Juliet. 
She had a good 
friend who was 
another little girl
named Wolfie.  One 
day, they were 
making slime at 
Juliet's house, 
when there came a 
knock on the window.
They looked out in
surprise, and saw
a grackle pecking
on the glass.  Most
surprising was that
this grackle was 
wearing a very 
small tophat.
    

...and turn it into this:


___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 19  20  9   26  27  10  7   19  20  27  25  27  24  22  12  26  5 

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 24  17  26  16  26  27  15  25  21  27  25  27  18  22  24  24  18  26

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 14  22  16  18  27  20  25  12  26  13  27  3   10  18  22  26  24  8 

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 21  17  26  27  17  25  13  27  25  27  14  19  19  13

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 4   16  22  26  20  13  27  15  17  19  27  15  25  21

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 25  20  19  24  17  26  16  27  18  22  24  24  18  26  27  14  22  16  18

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 20  25  12  26  13  27  15  19  18  4   22  26  8   27  27  19  20  26

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 13  25  6   5   27  24  17  26  6   27  15  26  16  26

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 12  25  11  22  20  14  27  21  18  22  12  26  27  25  24

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 3   10  18  22  26  24  2   21  27  17  19  10  21  26  5 

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 15  17  26  20  27  24  17  26  16  26  27  9   25  12  26  27  25

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 11  20  19  9   11  27  19  20  27  24  17  26  27  15  22  20  13  19  15  8 

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 24  17  26  6   27  18  19  19  11  26  13  27  19  10  24  27  22  20

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 21  10  16  7   16  22  21  26  5   27  25  20  13  27  21  25  15

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 25  27  14  16  25  9   11  18  26  27  7   26  9   11  22  20  14

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 19  20  27  24  17  26  27  14  18  25  21  21  8   27  27  12  19  21  24

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 21  10  16  7   16  22  21  22  20  14  27  15  25  21  27  24  17  25  24

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 24  17  22  21  27  14  16  25  9   11  18  26  27  15  25  21

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 15  26  25  16  22  20  14  27  25  27  1   26  16  6 

___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 21  12  25  18  18  27  24  19  7   17  25  24  8 


    
7*_ + -5 = 184    [space] = ___?
4*' + 5 = 13    [apostrophe] = ___?
4*, + 8 = 28    [comma] = ___?
-5*. + -6 = -46    [period] = ___?
8*a + -8 = 192    a = ___?
-1*c + -2 = -11    c = ___?
3*d + 6 = 45    d = ___?
-4*e + -3 = -107    e = ___?
-8*f + -5 = -37    f = ___?
-5*g + 5 = -65    g = ___?
6*h + -1 = 101    h = ___?
-4*i + 5 = -83    i = ___?
-4*j + -6 = -18    j = ___?
-8*k + -2 = -90    k = ___?
5*l + -7 = 83    l = ___?
-5*m + 4 = -56    m = ___?
1*n + -4 = 16    n = ___?
-2*o + 6 = -32    o = ___?
-2*p + -6 = -20    p = ___?
5*r + -1 = 79    r = ___?
-6*s + -6 = -132    s = ___?
7*t + -7 = 161    t = ___?
6*u + -2 = 58    u = ___?
8*v + 2 = 10    v = ___?
1*w + 8 = 23    w = ___?
-4*y + -4 = -28    y = ___?
    

p.s. So, we did this throughout June, and it worked pretty well. She quickly found that, by doing a few of the most common characters first (e.g. space, 'e', 't'), she could get a good guess as to others (e.g. if it's space-t-something-e-space, she could guess that the 'something' was 'h'). That way she could just confirm her guess, instead of having to do every algebra problem frontways like I intended. I decided this was ok, for a few reasons:

So, she would typically do half a dozen problems, and then the rest would be mixed about 50/50 between doing them normally or confirming a guess.

It also turned out that it took more time to do the filling in the answers after the algebra was done, than it did to solve the math problems. This was in part because she got faster and faster at doing the algebra part as June went by. At the beginning, she liked filling in the puzzle as a mental break from the math, but later just wanted to get on with solving the algebra, so I would sometimes help her out with filling in the answers, so that she could concentrate more on the math part. I should probably make a web page that would do the substitutions for her, but this will do for now. The main objective for July is to start making some slightly more involved algebra.