The Professional Steve

Tutorials, developer resources and inspiration.

Making A Game In Python Part 6: Explaining My Silly Little Function

Making_a_game_in_python_part_6

Here, for your reading enjoyment, are parts 1, 2, 3, 4, and 5 of this series as well as its dedicated page. Also, here is the github linkaroo.

Well, hopefully my previous posts on regular expressions (1, 2, 3, 4), function composition, and recursion explain this function:

def add_saved_data(string):
    matches = re.search("\{\{(.*?)\}\}", string)
    if(matches):
        default = matches.group(1)
        return add_saved_data(
                re.sub(
                    matches.group(0),
                    saved_data[default] if default in saved_data else default,
                    string))
    else:
        return string

Don’t panic, I’ll explain if you’re still lost. :)

Remember that this function is meant to be used for strings like this:

“Your name is {{player name}}. You’re in a darkly lit one-room house. Its raining outside. You can hear the drops hit the ceiling and can see rain hit the window when lightning strikes in the distance, which it often does. The window is above a sink full of dirty dishes. On the oven beside the sink there is a pot full of boiling water. To your left there is a couch facing a television. It’s turned to a channel that only gets static. Amazingly there’s a penguin sitting on the couch. The penguin turns to face you when you look at it. Behind you is a pile of smelly blankets and an old set of golf clubs.”

The goal is to replace stuff surrounded with curly braces (like {{player name}}) with game data like your name, the name of your penguin, whatever.

So the first thing the function does is look for something that is surrounded with curly braces using this regular expression: \{\{(.*?)\}\}. Because the { character is has a special meaning in regular expressions, we have to escape each instance of it with the \ character. If we didn’t have to do that the expression would look like {{(.*?)}}.

The parentheses are for group capture, it’s the thing we want to replace, like player name or penguin name. I’m not sure I mentioned this before but .* means match any character and the question mark right after it means “don’t be greedy”.

Yes, really, greedy is a technical term here. Normally, .* would match everything all the way to the end of the line. The dot, in regex, means “match anything”, it will match any character, including a closing } or two. To avoid it matching a closing curly brace, we put the question mark there to tell it not to.

So, let’s say that our function found {{player name}}. That means that matches.group(0) is {{player name}} and matches.group(1) is player name (no curly braces on the second one).

It also means that our if statement passes and the function will return something other than the string it was given. By the way, it’s important that we have that else here, otherwise we might keep calling this function over and over and over, blowing the call stack and creating a stack overflow, thus terminating our program.

So, in the first line after our if statement we get the default value for {{player name}}, which is player name. That’s how we set it up after all.

Next, we compose two functions re.sub, and add_saved_data. Namely we call re.sub first, then its return value is passed as a parameter to add_saved_data. That’s our recursive call.

re.sub takes three arguments: the substring we want to replace (here it’s {{player name}}), the string we’re replacing it with (I’ll talk about that bit of weirdness in a moment), and the the original string we started with.

Remember, the original string we started with is something that included {{player name}} or {{robot part number}} or whatever. We want to replace that substring with its default or with some bit of data the game remembers. re.sub will return the same string, but with the replacement we want.

So, what’s this bit?
saved_data[default] if default in saved_data else default

Well, if default is in saved_data (in our example we’re checking to see if player name is in saved_data, which it is) then Python will put saved_data[default] in this spot (in our example, this would be the actual name you typed in the game). Otherwise the default value (player name) goes in there.

Lastly the recursive call sends the new string (with the replacements we made) to a new call to add_saved_data. While there are still things to replace, the function will be called again. If not, it won’t, terminating the recursive loop.

And that’s how it works. Next time I’m gonna add more stuff to the game instead of explaining stuff (which will probably require me to explain more stuff :P ). Hmmmm…. what should I add next?

Recursion In Python

Recursion_in_Python

You are the captain of the space ship PythonPrize, which has been damaged beyond repair. You are your crew are setting up a temporary base in the caves of the non-aligned planet Molia, where you will defend a powerful alien artifact called The MagnaMacguffin until reinforcements arrive.

Suddenly your chief engineer, Lieutenant Higglesworth, runs up to you. “Captain! The Mole People of this planet have stolen the powerful MagnaMacguffin and hidden it deep within their underground lair!”

“When?” you ask.

“Just a few minutes ago. Our sensors have tracked it to a nearby underground compound.” She brings up a holo-display from her tablet, “Here’s a crude map:”
underground_map

“We’ve tipped the odds in our favor somewhat. We’ve temporarily disengaged their whole security system, but each subsystem will come back online if its sensor is triggered. For example, all doors in the compound are currently open, but once you pass through a door, it’ll reactivate and close, barring your way back out. Also, if we were to transport into their lair, their anti-transport shield would re-engage and no more transports to or from the compound would be possible.”

“Meaning that, even if we get in, we wouldn’t be about to transport out.” You furrow your brow thoughtfully. This looks bad.

“Thankfully we think we have a solution.” The engineer brings out a small, slightly luminescent, robot. It seems to be constantly vibrating, making it appear fuzzy and indistinct. “This is a recursive drone. It’s source code is written in such a way that it can call itself, instantly making a copy of itself.”

“A copy?” you ask, “I’m not sure I understand.”

The engineer takes the drone to a the entrance of a small, hastily built, maze in the middle of the temporary headquarters and activates it. The machine starts moving into the maze. You are astonished to see that, when it hits a spot in the maze where it could go one or more directions, it makes multiple copies of itself which go each way.

“Brilliant!” You say, “We can send one of these guys down the entrance, it’ll search the area it’s in, then make a copy of itself for each path it could go, and in this fashion we’ll be able to explore the whole compound and find out where they’re hiding the MagnaMacguffin!”

“That’s right sir! Closing doors won’t be a problem, because no robot will ever have to backtrack, and when one of the robots finds the MagnaMacguffin, it can attach a transporter homing beacon to the it and activate the beacon, automatically sending the artifact back to us!”

“And the MagnaMacguffin is back in our possession, it won’t matter if the anti-transporter shields are activated” you say while thoughtfully stroking your chin. “How soon can we send in the drone?”

“Just give us a few minutes to make a virtual test run and then we’ll do it for real.”

“Make it so,” you say, because that’s the way badass captains talk.

Lieutenant Higglesworth takes her robot back to its charging station and connects it up. She looks at the recursive function (written in Python of course) that drives the robots behavior:

def drone_search(lair_map, current_location, item_to_search_for):
    if find_item(item_to_search_for, current_location):
        place_beacon_and_activate()
    else:
        for new_location in current_location['paths']:
            drone_search(lair_map, lair_map[new_location], item_to_search_for)

The robot is given the map, its current location, and what item to search for. Then, if it finds the item it’s searching for, the drone places and activates the beacon. If it doesn’t find the item, it creates a new drone for every path it could take and sends each new drone to a new location, where the cycle starts over again.

She knows that it’s extremely important to write a recursive function so that, at some point, no new calls to that function are made. Otherwise, each time a function calls itself, it would call itself again and there would be way too many robots running around!

Also, Python can only hold so many functions in what it calls its “call stack.” Because of this limit there can only ever be 1000 functions in total running at the same time, and many of those functions will be taken up by Python itself, or by functions like find_item. However, there are only 11 locations in the mole people’s lair, so it’s not a worry. Still, if the number of functions called ever exceeded the call stack, there would be a stack overflow and the program would have to shut down. It’s something the lieutenant will have to consider any time she chooses to use recursion.

For test purposes only, the find_item and place_beacon_and_activate functions will look like this:

def find_item(item_to_search_for, current_location):
    return item_to_search_for in current_location['contents']

def place_beacon_and_activate():
    print 'beacon placed and activated'

On the real mission, she’ll use functions in the robot’s operating system that actually make the robot move and do things.

The lieutenant loads a data simulation of the mole people’s lair into the drone’s memory banks:

lair_map = {
    'entrance':{'contents':[], 'paths':['foyer']},
    'foyer':{'contents':['table', 'chair'], 'paths':['hall5', 'hall1', 'hall2']},
    'hall5':{'contents':['wall painting', 'drinking fountain'], 'paths':['arcade']},
    'hall1':{'contents':['mural'], 'paths':['mess hall']},
    'hall2':{'contents':['naked statue'], 'paths':['rec room']},
    'arcade':{'contents':['video game cabinets'], 'paths':[]},
    'mess hall':{'contents':['burgers', 'fries', 'quinoa'], 'paths':[]},
    'rec room':{'contents':['ping pong table', 'bean bag chairs'], 'paths':['hall3']},
    'hall3':{'contents':['skateboard ramp', 'penguin'], 'paths':['IT dept']},
    'IT dept':{'contents':['computers'], 'paths':['hall4']},
    'hall4':{'contents':['mangamacguffin'], 'paths':['customer service']},
    'customer service':{'contents':['poorly paid mole people', 'sorrow'], 'paths':[]}}

And runs the program:


..
.

It prints out beacon placed and activated!

“Now we’re ready for the real thing,” she says to herself.

Function Composition in Python

Function_Composition_In_Python

One thing I glossed over in my Python tutorials is function composition.

Lemme show you what I mean:

def sum(numbers):
    sum = 0;
    for number in numbers:
        sum += number
    return sum

print sum([1, 2, 3, 4])

In the above code, I create a function that takes an array of numbers and then returns their sum.

Now let me add another function that gets an array of numbers from the user and some code that makes use of it:

def sum(numbers):
    sum = 0;
    for number in numbers:
        sum += number
    return sum

def get_user_numbers():
    numbers = []
    still_getting_numbers = True
    while still_getting_numbers:
        user_input = raw_input("give me a number or q for quit: ")
        if user_input != 'q':
            number = float(user_input)
            numbers.append(number)
        else:
            still_getting_numbers = False
    return numbers

user_numbers = get_user_numbers()

total = sum(user_numbers)
total_as_string = str(total)

print "the sum of your numbers is " + total_as_string

This should all be pretty clear if you’ve gone through my earlier tutorials.

Ok, check this out:

def sum(numbers):
    sum = 0;
    for number in numbers:
        sum += number
    return sum

def get_user_numbers():
    numbers = []
    still_getting_numbers = True
    while still_getting_numbers:
        user_input = raw_input("give me a number or q for quit: ")
        if user_input != 'q':
            number = float(user_input)
            numbers.append(number)
        else:
            still_getting_numbers = False
    return numbers

total = sum(get_user_numbers())
total_as_string = str(total)

print "the sum of your numbers is " + total_as_string

You might not see it at first, but the change I made was to compose our two functions total = sum(get_user_numbers()).

Here’s how it breaks down: first Python calls get_user_numbers, then it takes that result (our array of numbers) and passes it onto the sum function, which Python then calls with the user-made array as an argument.

Let’s do some more of that compositional goodness:

def sum(numbers):
    sum = 0;
    for number in numbers:
        sum += number
    return sum

def get_user_numbers():
    numbers = []
    still_getting_numbers = True
    while still_getting_numbers:
        user_input = raw_input("give me a number or q for quit: ")
        if user_input != 'q':
            numbers.append(float(user_input))
        else:
            still_getting_numbers = False
    return numbers

print "the sum of your numbers is " + str(sum(get_user_numbers()))

Now Python calls get_user_numbers, then it passes the result of that function call to sum, which then gets passed to str. I also did a bit of function composition inside of get_user_numbers. Now we take the output of the float function call (which turns the input into a floating point number) and pass it to numbers.append.

Now, go forth and compose functions. You can nest them as deeply as you like!

Regular Expressions In Python Part 4: Character Classes

Regular_Expressions_In_Python_Part_4_Character_Classes

For your reading pleasure, here are partsĀ 1, 2, and 3 of this series.

There’s lots more to learn about regular expressions, but I’d rather get back to game programming. :) However, before we leave this subject, I’ve got one more thing to show you. If I didn’t show you this thing, I’d feel I’ve neglected my duties to you as a big-time professional blogger.

It’s a little thing called character classes. But first, let’s revisit our date checker from the last post.

import re

number = raw_input("enter date: ")

date = re.search('(\d{1,2})/(\d{1,2})/(\d{4}|\d{2})', number)

if date and date.group(1) and date.group(2) and date.group(3):
    print "Month: " + date.group(1)
    print "Day: " + date.group(2)
    print "Year: " + date.group(3)
else:
    print "nope!"

Does it bother you that something like 99/99/9999 gets accepted by this code?

Let’s fix that:

import re

number = raw_input("enter date: ")

date = re.search('(1[0-2]|0\d|\d)/(3[01]|[0-2]\d|\d)/([1-9]\d{3}|\d{2})', number)

if date and date.group(1) and date.group(2) and date.group(3):
    print "Month: " + date.group(1)
    print "Day: " + date.group(2)
    print "Year: " + date.group(3)
else:
    print "nope!"

So what the heck is with the square brackets? [0-2] matches any number between 0 and 2 (inclusive), [01] matches either a 0 or a 1 but nothing else, and [1-9] matches any number between 1 and 9 (inclusive).

You can put any characters you want in a character class and any range.

For example, let’s say you were building a regex to check for hex values. [A-Fa-f\d] will match any hex digit (be it lower case or capitalized) and nothing else.

Well, I think that about covers it for regex. The next topic I need to cover with you is a simple one, function composition.

I’ll write at you later. :P

Disappearing into React

Sorry I’ve not been posting y’all. Making regular posts is hard. :)

There’s been personal stuff as there always is, also there’s been another distraction. I’ve been getting really really into the React JavaScript framework.

I’ve even started a new project, an Impossible Store (app, github), where you will be able to buy impossible things that will be delivered at Alpha Centauri (it’ll be up to you to pick them up there. :P ).

A real post will be here on Monday, and every Monday hereafter (or until I change my schedule or am hit by an asteroid or eaten by a giant bird).

Regular Expressions In Python Part 3

groupFor your reading pleasure, here are parts 1 and 2 of this series.

In our last post I showed you how to see if a bit of text matched your standard phone number pattern, stuff like (123) 456-7890. Now what if you wanted to grab that number and do something with it?

import re

number = raw_input("enter phone number: ")

phone_number = re.search('^\s*(\(\d{3}\) \d{3}-\d{4})\s*$', number)

if phone_number:
    print "The number you entered is " + phone_number.group(1)
else:
    print "nope!"

This is almost exactly the same as the regex we used in part 2, but there are extra parentheses. The new opening parenthesis is right before the first old escaped parenthesis and the new closing parenthesis is right after our last 4-digit sequence. The part of the pattern enclosed in parentheses like this is the character group we want to capture, in this case the phone number.

The next new thing we’re doing is assigning the result of calling re.search to a variable. That’s so we can get the captured group. The call to phone_number.group(1) is where we actually grab the phone number.

Let’s try another example:

import re

number = raw_input("enter date: ")

date = re.search('(\d{2})/(\d{2})/(\d{4})', number)

if date and date.group(1) and date.group(2) and date.group(3):
    print "Month: " + date.group(1)
    print "Day: " + date.group(2)
    print "Year: " + date.group(3)
else:
    print "nope!"

So we can do capture more than one group with a regex. Group 1 was the month (2 digits, represented by the pattern \d{2}), group 2 the day (same as the month), and group 3 was the year (4 digits, represented by \d{4}).

Let me show you another trick:

import re

number = raw_input("enter date: ")

date = re.search('(\d{1,2})/(\d{1,2})/(\d{4}|\d{2})', number)

if date and date.group(1) and date.group(2) and date.group(3):
    print "Month: " + date.group(1)
    print "Day: " + date.group(2)
    print "Year: " + date.group(3)
else:
    print "nope!"

You should recognize the \d{1,2} business, that means we’ll match either 1 or 2 digits. But what is (\d{4}|\d{2}) all about? The | means or. This part of the pattern will match 4 digits or 2 digits, not three, not 1.

5 is right out.

Regular Expressions In Python Part 2

snake

In part 1 of the regular expressions series, I told you that this code:

import re

if re.search('a*', 'cucumber'):
    print "found it!"
else:
    print "didn't find it :("

prints out “found it!”

That’s because, in the world of regex, * means “match 0 to infinity of the last character”. a matches the pattern a*, so does aaaa, or an empty string. That might not sound useful now, but it will. Trust me, this stuff builds into something that you’ll wonder how you ever lived without.

The + character matches 1 or more of the previous character. batman matches a+ but cucumber doesn’t. ^ means the beginning of a string. joker matches the pattern ^j but banjo doesn’t. $ matches the end of a string. bangarang matches g$ but kangaroo misses the mark.

Time for another table:

character or set of characters what it matches
* 0 to infinity of the last character
+ 1 to infinity of the last character
^ the beginning of a string
$ the end of a string
? 0 or 1 of the previous character
\d The numbers 0 through 9
\D Any characters but the numbers 0 through 9
\w Any word character (alphanumeric mostly)
\W Any non-word character
\s Any white-space character, like spaces or tabs

Here’s a cool example: what if you had a program that wanted to see if a string matched a phone-number format, like (555) 555-5555? Here’s your regex:

import re

number = raw_input("enter phone number: ")

if re.search('^\(\d\d\d\) \d\d\d-\d\d\d\d$', number):
    print "it's valid!"
else:
    print "nope!"

Parentheses are a special regex character I’ll talk about in a later post, so I had to put a \ in front of them to escape them. Everything else should make sense. The first character ^ represents the beginning of the string and the last character $ represents the end of the string. That way we won’t say a number is valid if the user types in, say “blahblahblah(333) 333-3333eat my shorts”. The second character is an escaped parenthesis, then we put in 3 digit characters, another parenthesis, a space, 3 more digits, a dash, and then the final 4 digits.

Let’s do a few more tweaks:

import re

number = raw_input("enter phone number: ")

if re.search('^\s*\(\d{3}\) \d{3}-\d{4}\s*$', number):
    print "it's valid!"
else:
    print "nope!"

Here’s another cool regex feature. We can specify how many characters we want to match by putting that number after what we want to match. We just have to put it inside some curly braces. You can even include a range of numbers. For example \d{3,5} matches any strings with 3 to 5 digits in them.

I also added optional whitespace at the beginning and the end of the line with \s* just to make sure we ignore leading or trailing whitespace.

I hope you are starting to see the power of regular expressions for matching strings. In my next python regex post I’ll be talking about using regex to grab parts of strings for you. It’s called group capture.

Regular Expressions In Python Part 1

pieOk, first thing you need to know about regular expressions (or regex) is that they are an arcane magic you can use to do a lot with a little code. Second thing you need to know is that regular expressions are all about doing stuff with strings.

You get Python’s regex by importing the re module at the top of your python file, like so:

import re

Ok, let’s start with searching for patterns

import re

if re.search('a', 'a'):
    print "found it!"
else:
    print "didn't find it :("

If you run this code, you’ll print out “found it!”

Now replace

if re.search('a', 'a'):

with

if re.search('a', 'batman'):

You’ll still get a “found it!”

As you may have guessed, re.search takes the thing you give it in the first argument, and then searches for that thing in the second. We call that first thing a pattern.

Here are some more examples:

pattern string found it?
aa Banana didn’t find it :(
pie I like pie! found it!
hop Linus torvald is terrible at hopscotch found it!
a* cucumber found it!

Wait a minute, what’s up with that last one? How does ‘cucumber’ match the pattern ‘a*’? In regex patterns, the ‘*’ character has a special meaning. Are there more special regex characters? You bet your ass there are.

I’ll tell you more in part 2.

Making A Game In Python Part 5

I’m making a text-adventure in Python!

Here are the previous posts about this:
Making A Game In Python Part 1
Making A Game In Python Part 2
Making A Game In Python Part 3
Making A Game In Python Part 4

Here is the link to the code on github.

And here is the trello board I’m using to track my progress.

Ok, let’s get back to it!

Last time I said I wanted to have the game remember the name you give the penguin. I think I’ll create a separate data structure for this called “saved_data”. Later I’ll write some code to handle naming the penguin, but for now I’ll just add the player name to this structure:

player_name = raw_input("What's your name? ")

saved_data = {"player name" : player_name}

Next I’m going to change all instances where I typed something like this:

"Hello " + player_name + ", said the penguin"

To this:

"Hello {{player name}}, said the penguin"

Then I’ll write a function that takes “Hello {{player name}}, said the penguin”, looks at the saved data, and replaces “{{player name}}” with whatever’s in saved_data. If nothing is there, then it will use “player name” or whatever’s inside the double curly braces as the default. I’ll call the function add_saved_data.

So, if the player’s name is Reggie, then this code:

print add_saved_data("Hello {{player name}}, said the penguin")

should print out:
“Hello Reggie, said the penguin.”

The two places I want to call this function are when I print out a room’s description, and when I print out the player’s choices:

    print "\n" + add_saved_data(current_room["description"]) + "\n"

    for choice in current_room["choices"]:
        print choice["input"] + ") " + add_saved_data(choice["description"])
    print "q) quit game\n"

Ok, here’s the source code for add_saved_data:

def add_saved_data(string):
    matches = re.search("\{\{(.*?)\}\}", string)
    if(matches):
        default = matches.group(1)
        return add_saved_data(
                re.sub(
                    matches.group(0),
                    saved_data[default] if default in saved_data else default,
                    string))
    else:
        return string

Since this function is kind of complicated, I’m going to take a few posts to explain what it does.

In order to do that, I’ll first need to talk about regular expressions, function composition, and recursion.

Making A Game In Python Part 4

So, I’m making this game in python. Here are the previous posts about it:

Part 1
Part 2
Part 3

As always, the latest code for the game is available here on github.

Now, on with our adventure in making adventure…

In the last post we had developed a “real” game. Now I think it’s time to see how far we can get with what we’ve got. Before we add more code, let’s try and add some game content first: (again, you’ll have to scroll to the right to see it all)

game_data = {"one-room house" : {"description" : "You're name is " + player_name + ". You're in a darkly lit  one-room house. Its raining outside. You can hear the drops hit the ceiling and can see rain hit the window   when lightning strikes in the distance, which it often does. The window is above a sink full of dirty dishes. On the oven beside the sink there is a pot full of boiling water. To your left there is a couch facing a      television. It's turned to a channel that only gets static. Amazingly there's a penguin sitting on the couch. The penguin turns to face you when you look at it. Behind you is a pile of smelly blankets and an old set of  golf clubs.",
                                  "choices" : [{"input" : "t",
                                               "description" : "Talk to the penguin",
                                               "destination" : "penguin conversation"},
                                               {"input" : "l",
                                                "description" : "Look around the room again",
                                                "destination" : "one-room house"}]},
"penguin conversation" : {"description" : "You say, \"Hello penguin.\"\n\n\"Hello " + player_name + ",\"      replies the penguin.",
                          "choices" : [{"input" : "l",
                                        "description" : "look around the room again",
                                        "destination" : "one-room house"},
                                       {"input" : "a",
                                        "description" : "Ask the penguin what's going on.",
                                        "destination" : "penguin says what's up"},
                                       {"input" : "n",
                                        "description" : "Ask the penguin their name.",
                                        "destination" : "name the penguin"}]},
"penguin says what's up" : {"description" : "You say, \"What's going on?\"\n\n\"I heard a loud bang outside.  I think someone's out there.\" replies the penguin.\n\nJust then, you hear a loud noise. Some thing or some   one just hit the wall with a loud thud.",
                          "choices" : [{"input" : "l",
                                        "description" : "look around the room again",
                                        "destination" : "one-room house"},
                                       {"input" : "n",
                                        "description" : "Ask the penguin their name.",
                                        "destination" : "name the penguin"}]},
"name the penguin" : {"description" : "You say, \"What's your name?\"\n\nThe penguin says, \"You remember     it's...\"",
                          "choices" : [{"input" : "l",
                                        "description" : "look around the room again",
                                        "destination" : "one-room house"}]}}

I never did say in my tutorials what this \n or \” business is did I? \n means the program will output a new line. \” makes the program print the double quote character.

Already I can make a lot of game content with my little game program, but I can already see parts of the game that need expanding and improving.

I want to be able to name the penguin and have the game remember that name. If I name them, for example, Sparky, then I don’t want the game to call them “The Penguin” I want the game to call him Sparky.

Also, I don’t want to keep having to copy and paste the same choices over and over again. I pasted the “look around the room” choice a few times already. I should put them in another dictionary and then reference them in each scene.

I also think it’s high time I implemented the saving and loading of games. While I’m at it, I’d like the game to look for game files and ask if I want to make a new game with them.

There are a few other things I want to add, pretty much immediately.

I want an inventory, and I want to be able to have new choices when I have stuff in that inventory. For example, once I pick up a key, I should be able to open a lockbox I wasn’t able to open before. I should put a lockbox and key in that opening scene description to test this out.

I want to have some way to fight monsters, a battle engine.

And I should just build into the game that looking around is always a choice I can make. I don’t want to have to paste that choice into each scene. The game should remember my location and tell me what I should see when I look around.

Maybe I should tie certain choices to people, like the penguin, and locations? That might be getting ahead of myself.

Anyway, I’ll tackle the ‘name the penguin task’ next time. Also, I think it might be time to make a trello board. I’ll do that next post as well.

Follow

Get every new post delivered to your Inbox.

Join 209 other followers