Dungeons and Dragons and Python
Mike Pirnat
>> Everybody hear me okay back there? The
thing starts at 11:15, right?
Ready to go? Cool. Hi, everybody. Thanks for
coming today. My name is Mike. I'm a backend
engineer at Zapier, we're hiring, more on
that later. But I wanted today to share with
you something I've been working on in my spare
time but I thought we might mix it up a little
bit. So we're going to go on an adventure
together. We're going to explore a dungeon
while we also explore some Python code. So
I hope you brought your imagination. To help
you out in places where I'll be acting as
your dungeon master as well as your Python
guide, I will be wearing this enchanting hat.
[ Applause ]
All right. So the hat is now on. It begins
as these things are wont to do, at a tavern.
While relaxing at the Sleeping Parrot with
your trusty companion you're approached by
a man with a salt and pepper hair and a short
beard. You've never seen anyone who looks
like him before. He introduces himself as
a player of a player of a game of fantasy
and adventure called dungeons and dragons
but he needs your help to alleviate some of
his tedium. For years, you see, I've taken
a pencil and paper approach to preparing D
and D sessions. I need to encounter, I like
to copy the stat box off to an encounter sheet
so I don't have to look up in books later.
But this is painstaking and time consuming
and repetitive and takes hours and hours and
you get tired of copying the same thing over
and over again. This is a lot of work, even
for a short adventure, but it definitely does
not scale to a big campaign in the big hard
backs. Especially if it's a sandbox adventure
where you have issues with the plot being
able to fan out among paths, you're not sure
which direction your players are going to
go we also have a lot of tedium around keeping
track of whose turn it is and how long various
things are in effect. For example if running
an encounter, everybody has their own initiative,
we maybe copy those names out on to a numbered
list and it goes from highest to lowest and
that repeats again until the encounter is
concluded. This can be a lot of extra rolling
for the DM and it is really easy and also
very embarrassing to lose track of whose turn
it is. I've done this in live play and it
really makes me feel bad. So that's just the
start of it. There's a lot to track, it can
be tedious. We're at a point in 2019 where
pencil and paper have become too tiresome
and inefficient. Now eventually D & D beyond
will solve all my problems but it's taking
two years. My need is immediate and I don't
need all that fancy web UI. I'm looking forward
to being put out of business by the big boys
here.
Now I've heard, he says, that you have some
experience with the language of the snake
cult.
[ Laughter ]
I believe that you if were to visit their
temple, you might bring back some magical
knowledge that will ease my burdens. Will
you do it? What say you? All right! So exposure.
So the man offers a slender metal rectangle
that bears the fruited mark of Cupertignome.
It opens like a clam shell to reveal tiny
keys. There's a flash of light and you and
Pip are transported into darkness. As you're
starting to worry about whether you'll be
eaten by a Gru, you suddenly hear voices.
Welcome to our temple, it is a sssspecial
place where incantations of Python code become
a reality. We're terribly ssorry about hospitality.
All of the snake artists were attending PyWaterdeep.
We'll grant you the knowledge you can seek
if you can show your ssskill in five challenges.
In the darkness you hear something that sounds
like slithering. You're in a large room, slightly
wider than it is deep with a smooth obsidian
floor. Evenly spaced columns run along both
sides of the room. You've guessed maybe 80
columns total. In the center of the room is
a bag containing tools, parts and a scroll
labeled read me. You begin to read and tivrner.
I grew up into zork series and a termentnal
app feels like a great fit for my need. Prompt-Toolkit
is awesome for this. It is a library for creating
interactive command line and termination applications
in Python. It supports a ton of features.
Auto suggestion, auto completion, key bindings,
multiline input and a whole lot more. So let's
get started doing that. So we need to import
a prompt method and all we have to do is ask
for user input. We ask it for prompts, the
string that's in quotes there is what gets
displayed in the terminal and let's echo it
back. And let's wrap that in an endless loop
so we can keep taking those commands until
we're done playing. There are lots of ways
we can customize the prompt we might ask for
more input in applications than just this
main prompt so instead we can use a prompt
session. We just replace the prompt method
with import of prompt sessions, spin up that
session object and we can call it prompt method.
That way any time we call that it will behave
consistently and we won't have to pass any
other arguments to it.
The obsidian floor begins to flow with words
and symbols that appear on it and it looks
like this. So we can see it taking text and
echoing back. Nice and simple. Since we have
a prompt session, we can have it suggest things
that we've entered previously as we start
typing. So let's see what that does. So now
you can see it through that grayed out thing
that came from our history. We can also wire
up tech suggestion and completion based on,
say, a set of known words. So we can use a
word completer which is something that comes
out of the box, give it some words that we're
likely to type, and spin up that word completer.
And pass that into the prompt session. And
when we look at what that does, we can see
it start offering a menu of choices. And you
can navigate that with a keyboard or it will
start narrowing down those as you type.
So a nice thing that we might want to do is
also add a bottom tool bar to our application
so that we have some display that is nice,
because this makes it feel a little more polished
then just typing in a blank terminal window.
So we can make a function called bottom tool
bar and we'll pass that to the prompt session
as bottom tool bar equals our function and
it starts displaying whatever text we want
it to have. Now we have a nice clue to get
out of this however we want to exit.
We can also take our bottom tool bar and instead
of returning a string, we can change that
to a list of tupples. We can add our text
back in. To make it pretty we will import
the style class, make a style object using
its method -- a string that specifies the
name of the class and the coloring we're going
to apply and pass that into the prompt session
and it looks like that and we have some nicely
styled text and we've also changed the background
color of that bottom tool bar.
So as we completely these words, a section
of the stone wall to the west slides open.
Revealing a passage that curls away to the
north. You and Pip go into the tunnel, then
the floor suddenly drops away. You tumble
through a winding tube that deposits you into
a circular room about 40 feet acontracts.
Four levers are ensconced in the smooth stone
walls. As you study the room you hear a click
sound from the floor and a stone panel slides
down behind you blocking the way that you
arrived. You hear the sound of stone against
stone, mighty gears in motion, the ceiling
begins to slowlying down on you. You and Pip
begin to pull on the levers but they have
no effect. I thought we have to pull all four
levers at once. Pip pulls out tunnel adders
and click. These should help, they say. So
TOML is tomorrow's obvious minimal language.
It's a data serialization language designed
for files with obvious semantics. It's an
alternative to YAML and JSON. Because it's
plain text we can keep it in git and version
it and branch it which is going to come in
handy when we want to customize and tune into
the party that we're working with. Here's
some TOML that represents what a dungeon master
needs to know about a character. She's got
a name, hit points. When we parse that we
get a Python dictionary that looks like this.
So you can see here that the text between
those brackets, like the square brackets up
there, becomes a key in that dictionary, and
the name value pairs that are beneath it become
the contents of that dictionary nesting it
nicely in there. And you can make an additional
nested dictionary by using the dot operator.
So we hang that senses dictionary off of the
serial dictionary. So here's a more involved
example. This is just sort of some highlights
of it. This represents a monster stack block.
This is a thing I would have to copy out by
hand. Now I only have to type it once. Strength,
dexterity, any features that it has, the nimble
escape ability. You also check out the triple
quoted strings just like Python. TOML looks
just like Python. Once we have monsters we
can compose them by groups into encounters.
So an encounter might have a name, a location,
some notes and knows groups of monsters that
are going to be present. No more copying the
stuff out longhand on every single page. Super
great.
So now it's easy to define that data and bring
it in to Python. It would be nice to have
some proper model objects to put it into and
that's where adders come in. It's a great
library to create classes without a bunch
of boilerplate that spares us from meaningless
methods that take arguments and same them
as attributes on your object. Attrs is great
when you want a data class, but a data class
that does a bit more stuff. And it has more
minimal syntax that you can use, a more verbose
syntax, I prefer the latter because I apparently
don't have a sense of humor. We'll I am support
attrs and attrib -- 
define a bunch of attributes and give them
some same defaults. We can have them be strings
or integers or whatever. But if the default
is going to be a mutable thing like a dictionary,
we're going to need to use a factory function
to make sure we get a fresh dictionary every
time, otherwise every character is going to
end up sharing the same dictionary and we
don't want that. We don't want people's attributes
beaking back and forth between characters.
In this case we use a factory object, I like
to call that an attr factory and pass a cobble
into it so it knows how to stamp out your
default into it. We'll import the TOML library,
open our file up for reading with all of our
party characters in it, we'll load them like
we saw earlier with TOML.load, and using a
dictionary complengs we can make dictionary
of character objects, keyed by the character's
name, so we can easily find them later. It's
a lot easier to look up Sariel or Lander or
one of these other people. We'll pass the
name value pairs from the TOML data using
the star star operator and Attrs will take
care of the rest. It's almost like magic.
So then we might want to know where our party
file is coming from or be able to use different
ones at different times. So click is a helpful
library for this. Click helps us to write
command interfaces. It makes it easy to wire
up parameters and it makes nice output. It's
well-covered in other talks. I'm not going
to get into a ton of detail today. Let's turn
to our prompt loop and turn that into a function
that our program is going to call when it
starts up. Then we'll import Click and we'll
apply the click.command decorator so click
is going have an entry point into our program.
We can add options using the click.option
decorator and include help text or defaults
and all kinds of other fun stuff. Magically
as you add that option, you start getting
arguments passed into that function. So as
we're taking a party argument, we'll have
a party parameter going into the main loop.
So you might want to save off that value so
we can remember that for later and if we had
one specified, it will be none if we don't.
If we have one specified, we can go load up
all of our characters. If you wanted to add
more options, you just add more click dot
option decorators. So in this case, where
do we find our pool of monsters to draw from,
where do we find our pool of encounters to
draw from. Let's take our more simpler example
that we were looking at, let's see what that
looks like.
So we can see, this is what our TOML file
looked like and as we start up our example,
we can see help text happening, and we've
loaded some characters. Cool? Cool. As you
speak these words of the Python spell, our
friend Sariel and Pip crawling quickly to
avoid the crushing weight that's descending
on you. The four of you pull the levers together
and the ceiling rises once more. Three stone
doors slide open. Revealing exits to the north,
the west and the south. To the north and west
stairs lead down and to the south stairs lead
up. Which way do we want to go?
MIKE: We hear the sound of a pond to the north.
We'll go north. The passage connects the tunnel.
You found stairs that descend deeply into
a tang willed mess of thick cobwebs. Giant
spiders, says Sariel the ranger. I would stake
my life on it. Let's turn around, says Pip.
We don't have time for web development.
[ Laughter ]
Lander, and the rest of the audience, groan
disapprovingly. To the south the stairs lead
up. The passage turn to the west and you go
up some more stairs into a large, mostly square
room with a gently curved wall to the west.
Thick columns rise up to an arched ceiling.
In the middle of the room is a large stone
pit filled with small wriggling snakes. A
stone door appears to be locked. Sariel observes
an Elvish inscription on the door she translates
and says command the serpents and they shall
become your instruments.
So let's make our application do something.
Two of the really core things are reeling
dice and keeping track of whose turn it is.
So let's start with dice. Rolling dice is
fun. These weird expressions tell us how to
role dice. The first number tell us how much
of a die to role. And the D whatever tells
us what shape of die we're going to be rolling.
These are polyhedral dice. Some might have
four or eight or six sides. And we might have
some plus or minus to adjust the results after
we sum up everything that we've rolled. In
the case of, we're going to role 3 4-sided
dice and add 3 to it. Let's make a command
that can roll dice. We'll make a function
that takes those dice parameters we just looked
at, start with a nice place to start 0, we'll
roll as many times as we're told to, and get
our random number and add it to the result.
We'll roll between 1 and the number of sides
that we have.
And then return that, plus the modifier. Or,
if you're really into code golf, you can make
this a one liner, but it might not be as clear.
This is kind of fun to do, but I don't know
that I recommend it all the time. But we really
want to turn one of these expressions, a string,
into a rolled number. So that means it's time
for a regular expression. So this regular
expression tries to parse a string into how
many dice we're going to roll, how many sides
the die will have, and whether we need to
add or subtract anything from the total. We
use those parentheses to isolate the groups
that we're going to remember for later. We
don't want the plus inside the last group
because it won't turn into an int nicely but
we do want the minus if it's there.
Our new function will take a dice expression
string and try to match it against that regular
expression. Now, this would be a really great
place for a wall rust operator. I'm really
looking forward to 3.8. I love it so much,
Walrus operator. If we match, we can get those
key parameters out of the groups that we captured
and we'll turn them into ints so we can do
math with them and all we need to do is call
our other roll dice function to do the work
and return our result. We need to update our
prompt loop to do that dice rolling. We'll
split the user input on spaces. The first
item will choose the command we execute and
everything else will be arguments for that
command. And if we don't get any user input,
we'll go back and prompt again. So now if
our command is roll, we will go and call that
roll dice expression, and it might be nice
to roll as many as we want. So you could roll
1D20, 1D20 and get multiple different rolls.
In the current version of dungeons and dragons
we often do 1D20 rolls at a time to do what
we call advantage or disadvantage. If you
got advantage you roll 2 if you have disadvantage
you roll 2 and take the worst one. You want
to make it easy to do that. And then we'll
admit the results. Join them up nicely with
a comma there. If we don't have a command
that we know, let's complain about it so we
get some feedback to the user.
So let's see that in action. I've gone past
my video. There we go. Here we're rolling
some things, we can see it still computering.
Here we're taking a couple of -- rolling with
advantage and disadvantage to do some things
and we're rolling with modifiers. Random numbers,
rolling dice. So now we need to take turns
with initiative. D & D as we mentioned earlier
has this thing called initiative we roll a
20 and go from highest to lowest to determine
who goes when. Let's say we all roll for initiative
and if we write down results it might look
like this. Sariel rolled a 20, those goblins
rolled a 13. Lander goblin 4 and Pip rolled
a 10 and so forth. To me thats a lot like
a dictionary of lists. So let's use that as
the core of our turn tracker. We'll use a
default dict so it's easier to add new combat
ants into the turn order. Any time we reference
some value, it will create a new list with
that roll as a key and an empty list we can
start putting things into. We want to keep
track also of the round number so we can understand
the passage of time during combat. We need
to be able to add someone to the turn order,
and this appends them into the right spot
in that default dictionary. We'll also need
to remove combatants for various reasons.
Maybe they've disappeared for some reason,
so we'll walk through the different combatants
in the initiative dictionary. And one catch
here, as we try to remove them, is that list
remove method will throw an exception if the
item we're trying to remove isn't actually
in the list. So we need to check first to
see if we're there before we try do it.
Now, let's make a generator to give us the
next turn. We're going to loop through all
the combatants and repeat and repeat until
we're done with our combat. So each trip through
the loop is going to represent a round of
combat. We'll increment the round number,
get the combatant lists from highest to lowest
that's why we've got the reverse sorted. We'll
loop across all of those combatant groups
that we get, and to get, we get each combatant
in turn and walk over them. And we'll yield
some info about where we are, like the round
number and whether we're in 20 or 13 or 10
or whatever, and whose turn it is now. And
to advance this one turn at a time, rather
than consuming the entire list, because we
would consume this endlessly, we want to explicitly
call next on our generator. So first we call
generate turns to return that generator object
to get the actual generator we can use. And
then we can start looping over. So let's say
we generated some output with a 4 loop here
and we'll do next on turns. Each time we want
to advance a turn, we'll call next with that
turn generator and it does something like
this. We omit round 1, initiative 20, round
1 initiative 13 and so forth it becomes the
second round or 42nd round as time passes.
This is pretty great, but there is an embarrassing
bug here. Do you see it? So if you look at
that initiative dictionary, let's say we're
in initiative 10, and it's either Lander or
goblin 4's turn. Maybe they're defeated and
it happens before the turn order gets to Pip.
Because we modified the lists that we're iterating
over, Pip actually gets skipped and now it's
goblin 2's turn. I had this happen to me because
Pip was the person I skipped accidentally
in a paper and pencil session. I was really
glad to fix this. So we can fix this by making
a copy of that combatants group before we
loop across it and we skip any combatant who
is in that copy but is no longer in the real
list. Now when goblin 4 is removed, play proceeds
as expected and I don't forget Pip anymore.
The nice thing we might do is wrap this up
in a class so our state is better encapsulated
and we have this nice tour object to work
with. We need to manage that next command,
we will prompt it for getting the next turn,
the round number the initiative and combatant
and we'll print out the result and it looks
kind of like this. So you can see we've added
some initiative and we're looping through
and hurray, it advances the turn.
So Sariel uses her animal handling skills
as a ranger, and she rolled an 18. She's really
good. She reaches into the pit of snakes and
draws one out. As she commands it to be a
set of dice, the snake becomes a set of dice
in her hands. Sariel scoops out more snakes
and as you invent various commands together
they become rods, rings and other devices.
You produce a key, it fits the keyhole and
the door unlocks. Gathering your possessions,
you head east on a long passage and emerge
into a similarly shaped room curved this time
on an eastern wall. Long shelves along the
north and south walls hold a series of identical
boxes with hinged lids. A closed stone door
on the west wall won't budge, and there's
no keyhole this time. On the east side of
the room, several steps lead up to a wide
dais. On the wall is a designed carved into
the stone. It looks like this a rectangle
connected by arrows each bounding a niche
that's cut into the wall. Lander studies them
for a while and object serves they're about
the same size as those boxes over on the shelves.
He makes a history check and let's say for
the sake of the plot that he rolled an actual
20. A-ha, he exclamation, this looks like
a class diagram. It would be a lot easier
you think for all of those commands we just
made to be a little more uniform and more
easily dispatched. So up until now we've just
been cramming commands into this long and
ever-growing chain of ifs 
and elifs. Here it's a good time to think
about moving some code around so it all isn't
in that giant pile. We might pull out the
dice rolling and the turn tracking code, but
what really matters for this next section
is we want to put each of our commands into
a separate file within a commands directory
that has an under an it py so we can import
things out of it. So let's make a command
class. It's going to have one or more key
words, and of keep a reference to a global
game state object that will be useful later
and we'll tell the game's decksry of commands
that each of our key words points to this
object and we'll let the user know we've registered
this as a command we can do by inspecting
the class name of what we just registered.
Great. Now it's registered as something we
can do. We'll need a thing for it to do. So
let's make a do command function. It will
take some arguments. And our base class shouldn't
do much. And it might be nice also to add
a hook for showing interactive help text so
you can remember how things work while you're
playing the game. I use this a lot. It will
just print out the help text, strip out any
space around the end. But maybe we want to
make this possible for command to not blow
up the game if it doesn't have any help text
on it. So we'll check that with a get attr
first and print out a message that there doesn't
have any help text available if we don't find
any.
Let's wrap up our dice rolling command. We'll
make a command subclass. We'll give it some
key words so we can use either roll or dice
to invoke the dice rolling. We'll give it
some help text eventually and then we'll do
the command. We'll take our existing dice
rolling code out of that big pile we had and
put it in here. We'll do that same list comprehension
to roll out the dies and print out the results.
The next turn command is similar. It does
the same sort of thing, advancing the turn
using that next to call to advance the generator
that we set up. Now we have a lot of classes.
We can manually import those commands and
register them when our program starts up,
and I did that for a while. But it gets tiresome
and it's easy to forget. If you've built out
a command, it's easy to forget to register
it and you start it up to demo it and there's
no command why is there no command. So wouldn't
it be nice if we could find and register all
of our commands dynamically. So we'll need
some more friends from the standard library
for this, particularly import lib and package
util. We'll use path to figure out where they
live. We'll use pkgutil to find all the Python
modules in that directory and look at what
we found. If we've already loaded a module
up with a particular name, we'll skip over
it so we don't clobber it, otherwise we'll
import that module using importlib. In this
case we'll use the module name to determine
the class name in that model we're going to
instantiate. I chose to make a snake cased
module name into a camel cased class name.
We're splitting on the underscores and gluing
it all together. Once we know the class's
name we can try to acquire it and skip it
if we don't find the class we expect and finally
instantiate that instance and let it go register
itself.
So now we can clean up all that ungainly dispatching
by throwing almost all of this away. So we
want to make sure our commands are loaded.
And then we'll get the command name from the
user input and use that to look up the command
class that we want, or the command object
that we want to use. If we find one, we go
ahead and run it. Otherwise we just complain
to the user that we don't know that command.
So all of that garbage gone and we have this
nice tidy package. So let's see how that works.
We can see it register our next turn in our
roll dice commands and everything still works.
Sweet.
So as we finish this incantation, the boxes
on the wall begin to glow. You and your party
place the snake command items one by one into
the boxes, and place the boxes into the niches
on the diagram. The diagram itself begins
to glow and the stone door slides open. Whoosh.
You follow the passageway down two flights
of stairs and it turns sharply south. Opening
at least into what looks like the same obsidian
floored room that you arrived in. Pip looks
stricken. Will our journey never be complete,
they wail? Complete that gives you an idea.
We talked about completion earlier and let's
do it better now. We can manually create a
set of words to feed into our word completer
but it might be better to automatically discover
them from the registered commands. So we can
pull that out of the keys in our commands
dictionary. But eventually you want more sophisticated
commands with more sophisticated arguments
should be appropriate to the context to the
position of the arguments within the only
structure of arguments being typed in. So
we'll make a command completer, and we'll
need some things to work with. We'll import
completer and completion. Our command completer
will subclass. There's more it can do in a
typical completer but this is sort of a minimal
for us. We'll remember all the command objects
as well as a sorted list of all of their keywords.
The most important part is providing a get
completions method. This needs to be a generator
that yields completion objects. This is get
a little gnarly. This method is going to do
a few things. It's to figure out what's been
typed so far, figure out what completions
we might suggest, figure out if a completion
is valid and yield out each valid completion.
This is going to go pretty fast. We need to
remember what's been typed so far, so we'll
call that the word before the cursor, that's
the thin that we get from the document object
that Prompt-Toolkit gives us, what's been
typed so far right now. We might want to ignore
the case. And in case or in that case lowercase
the word before the cursor. Now we want to
find out what completions we might want to
suggest. We'll start with an empty list of
suggestions. We'll split the document text
list that we had on spaces there. And this
will help you understand if we have a single
word that we're at or if we have multiple
words chained together. For the first position
we only had zero or one thing typed so far,
we're going to look at the base commands,
the list of all of those command keywords
as our initial suggestions and offer those
up. Otherwise if we already have that first
word there and we know if it's one of our
registered commands, something we know about,
we can reach into it and grab the command
object from all the commands remembered and
ask the command to comply it's own suggestions
based on what's been typed and how far along
we are. So now -- cool. So now we make a quick
function to see if the typed text matches
up with a suggestion option. Or more to the
point, to see if a suggestion option matches
up with the typed text. So we'll use this
to know what suggestions to show the user
and change them as they type. So we can customize
how the matching works, either ignoring the
case or matching from the middle of the string
or matching from the beginning of the string.
So we'll pass that suggestion in to see if
it matches the word we're looking at that's
been typed so far. Otherwise we'll return
with the start position, matching it from
the start of the word. Next we want to look
at all the suggestions that we have, and yield
all the valid ones. And see if they match
what's been typed. So we check that word matches
function. If a suggestion checks out, we yield
a completion object that knows what suggestion
to show and where to position it into the
input buffer. Now, we need to make this negative,
because we want to step back a few characters
and overwrite the word that's been typed.
Otherwise what we're going to end up with
is a mangled input that concatenates what
has been typed with what was suggested. So
if the command was dice, and you type di and
completed dice you would have di-dice. You
would be upset. We don't want that. Upset
is exactly the opposite of what we want. All
done. Did everybody pass your intelligence
saves? Sort of. This was a little weird the
first couple of times that I messed with this
too. There's a lot more things that you can
do with it that I'm not going to go into here.
Great. Now we need to make our commands class
know we can offer suggestions. Say we have
a command like damage combatants that can
do damage to one or more people in the game
or creatures in the game. It should suggest
combatants, so it will have a get suggestions
method, and we would like to look at everybody
that's already been specified. So if you've
typed three characters' names, those characters
might not want to show as suggestions because
you've already said you're going to damage
that creature. So we'll subtract everyone
who's already been chosen from the set of
all the people that we might interact with.
Giving us a sorted list of combatants that
haven't been chosen yet. And this looks kind
of like this. So we can see as we go that
as we type we're removing some of those suggestions
and we'll have one here where I think we're
going to do three people. So on Lander's turn
maybe a trap happens. And you see each time
there are fewer suggestions, as we go.
Oh, right.
So as this spell is completed, sections of
the obsidian floor suddenly slide upward with
a grinding sound and a tremendous noise forming
a spiral care case that leads up and out of
the room through an opening you hadn't previously
seen. Ascending the stairs, you emerge into
a vast chamber of dark stone -- somebody got
it, good. Ringed with stairs that lead up
and out of the temple. Two giant blue and
gold serpents appear from the shadows and
begin to speak. You recognize the voices that
you heard earlier in the darkness. Well done.
You've learned much, says the first. And earned
your reward. In your hands you suddenly find
scrolls filled with code, examples and documentation.
Will we sssee you again says the other? Perhaps
at PyNeverwinter. The CFP is open. The serpents
laugh. There's another bright flash and you
and your companions are back at the tavern.
Your client seems excited and starts listing
off all the ideas he has for few features
and commands but for you, for now, it's time
to celebrate the conclusion of another adventure.
All right. Thank you.
[ Applause ]
So this has been just a tiny look at what's
been possible and only a fraction of what
I've implemented so far. What you've seen
today are the fundamental building blocks
that everything else follows from. I don't
have a ton of time today and I want to make
sure that you get to lunch soon, get in on
the yummy sandwiches but I would be delighted
to sit down for some one-on-ones, do some
demos. You can see all kinds of things like
splitting the party or re-Janing the party
or stashing and unstashing loading encounters,
navigating actual combat situations and see
what game play is like with this tool. But
if you don't want to do that, that's also
cool. You can check out these links. I will
be posting the slides soon, so don't fret.
I'll about on the Twitters with that. You
can follow me on Twitter to learn more. That
is the actual tool I've been working O I would
be glad to chat here and on Twitter too. I
want to thank the authors of these great tools.
They've been really helpful and I've gotten
a ton of mileage out of them. Thanks to all
the folks responsible for them. Quick thanks
for all of the resources that I borrowed to
put this together. I did really enjoy handdrawing
the dungeon on pencil and graph paper getting
it scanned in. It really took me back. It
was a lot of fun. I also want to thank Zapier
for making it possible for me to be here today.
We are also seeking brave adventurers to join
us in our quest to democratize automation.
We have positions open for engineering wizards,
support paladins and sore sourcers and more.
Come talk to me about what it's like to work
for an all remote company that does neat things
with Python and is full of the most wonderful
people. And finally thanks to you for coming
to this talk. I hope you had fun and learned
something and I hope that you have an excellent,
excellent day. Take care.
[ Applause ]
