Generating NPC code in a Pokemon game
Generating NPC code in a Pokemon game
2019-04-13

Recently I've really been into Poké-MMO style games, basically unofficial fan-made Pokémon games that allow you to play in a shared world with everyone else.

For the uninitiated: Pokémon is a game in which you tame magical beast and make them battle each other (cruel I know). Each player has a "team" made of 6 Pokémon, analogous to a soccer team but with fire horses, metal praying mantis and water spraying ducks. There are hundreds of different Pokémon species!

I've been so into it in fact, that I joined the volunteer staffing crew that runs Pokémon Revolution Online (PRO) as a NPC scripter!

PRO logo Pikachu

In this game, NPC (basically game characters) can be classified in 3 categories:

Boss NPC are designed as a challenge to strong players and can impose weird requirement like "player must use a team made of Water-type Pokémon only" to make the battle harder.

Aside from their Pokémon team, Boss NPC have 3 parts that we must script:

Oh and there's one more thing to know about them: Boss NPC are very popular. Most of the community requests to NPC scripters were about them.

So a boss NPC script is

  1. Something that is done on a computer
  2. with a regular structure
  3. of which production is in high demand

... You might've guessed it already, after scripting a whole 2 Boss NPC, I decided to automate it.

The script syntax

Since we're about to automate scripting, let's take a look at an example script that our program should generate:

#!python
# Generated by PRO BossMaker Discord bot - please report any problems to Inspi#8989
from random import randrange

# cooldown time to prevent the player from farming the NPC
if user.expire.AQUABOSS_CD is not None:
    hours, remainder = divmod(user.expire.AQUABOSS_CD.total_seconds(), 3600)
    mins = remainder/60
    user.say(f"I'm tired, come back in [FF3300]{hours} hours, {mins} minutes[-].")
    return

# Condition 1: Team must be Water-type only
if not all("Water" in poke.types for poke in user.team):
    user.say("You need a team of only Water-type Pokémon to challenge me!")
    return

# Ask if the player wants to battle
choice = user.select(f"So, you think you can defeat me, [3399FF]{user.username}[-]?", ["Let's go", "Uh, no"])
if choice[0] == 1:
    # the player declines the battle
    user.say("Fine, come back when you're ready.")
    return

# the player agreed to battle
user.say("Prepare yourself!")
user.pause()
outcome = user.battle(npc, noexp=True, no_teleport=True)
user.pokes.heal()
user.pause()
if outcome == 0:
    # the player lost
    user.AQUABOSS_CD.set(1, hours=2)
    user.say("Not quite there yet, try again later.")
    return

# the player won
user.say("Impressive! You truly are a skilled trainer.")
# set win CD
user.AQUABOSS_CD.set(1, days=1)

# REWARDS 
reward = randrange(1,100)
if reward <= 66: # 66% chance to get Pokédollars
    money = randrange(100, 500)
    user.money += money
    user.play_sound(1)
    user.say(f"You got [FFCC00]{money} pokedollars[-]!")
    user.pause()
elif reward <= 100: # 33% chance to get items instead
    user.items["hyper ball"] += 10
    user.play_sound(1)
    user.say("You got [00AA00]10 Hyper Ball[-]!")
    user.pause()

The script is pretty straightforward to write, but generating it is another story.

Right away we can identify the tricky parts:

You may notice that the NPC Pokémon team is nowhere to be found in the script. That's because it's specified elsewhere, we won't look into that in this post :)

The user syntax

Now that we've seen what the output looks like, it's time to think about how we want the input to look like.

To make our life easy, the input should be a text file with an intuitive syntax, it should be accessible to someone with 0 programming experience.

This text file should have 3 parts: requirements, rewards and dialogue. Here's what they could look like for our "Aqua Boss" NPC:

conditions:
have only water type

This part is easy to understand, we could write as many requirement as we like to allow the battle, one per line.

rewards:
money 100 to 500 2
10 Hyper Ball 1

For the reward section the idea is to list all the possible rewards with a number attached at the end (here it's "2" and "1"). This number would define the odds of getting said reward, relative to the others. So here the player is 2 times more likely to get Pokédollars (in-game money) than 10 Hyper balls, resulting in 66% odds for the Pokédollars and 33% for the 10 Hyper balls.

This is more convenient than working with percentages% because we don't need to check that it adds up to 100%! We can just continue adding rewards to the pool without ever bothering to re-normalize the other %.

dialogues:
meet: So, you think you can defeat me, %name%?
yes: Let's go
no: Uh, no
battle: Prepare yourself!
decline: Fine, come back when you're ready.
win: Impressive! You truly are a skilled trainer.
loose: Not quite there yet, try again later.
on cd: I'm tired, come back in %cd%.
condition 1: You need a team of only Water-type Pokémon to challenge me!

The dialogue specification is also easy to understand, predefined actions are associated with a dialogue line. We do need however to make some variables available such as %name% for the player name and %cd% for the cool-down time.

The hard part

So we put a simple text file in the machine, almost in plain english, and we get a functional script in return, that we can just put in the game and it works ?

Sounds like magic.

And indeed, for me, it kinda is! What we're doing here is similar (though much much simpler) to what a compiler is doing when it reads human code and transforms it into computer instructions, and I've never done anything of the like before.

Since it was my first time writing a Parser, I got the help of a good friend of mine Chris Janaqi which I'm very grateful for.

1. Define "plain english"

Of course, our User script is not exactly written in plain english. It has a specific syntax that has to be respected for the machine to understand it. This syntax needs to be formally defined (boring I know).

Here's what the syntax for the "have" requirement looks like for example:

have <count> (OT) ([<pokemon>, <typename> type]) ([above, under] level <levelvalue>)

For Pokémon nerds: "OT" means "Original Trainer", meaning the Pokémon needs to be yours :p

If you've ever used a command line terminal, you probably understand this syntax specification!

The first word is "have" so this is the "have" requirement.

Here's a few example requirements that this syntax allows you to write:

And that's it! For the "have" syntax at least. Every other line in the text file follows a syntax that we formally define.

2. Open VS Code

Now that we've done ALL of the work defined above, it's finally time to write our magic machine that will read the almost-plain-english text file and output a working NPC script!

Btw our "magic machine" is written in Python. And the NPC script it produces? Also Python.
So it's python code that produces python code, scary!

This machine has 3 parts:

These parts are actually the standard for most Parser-generator projects :)

The strict separation between input and output by the IR allows us to make modifications to the user facing syntax or the NPC script syntax without affecting the rest of the project.

The Intermediate Representation

So the IR is basically just Python classes that represent everything that we need in order to generate the NPC script.

Here's the IR that would represent the "have" requirement for example:

@dataclass
class HaveRequirement:
    count: str
    ot: bool
    pokemon: str
    pokemon_type: str
    min_level: int
    max_level: int

This allows us to store every info that our formally defined syntax allows to write.

Importantly: IR is just data, don't put any behavior in there!

The parser

So the parser is the part that reads the text file following our formal syntax that looks like this:
have <count> (OT) ([<pokemon>, <typename> type]) ([above, under] level <levelvalue>)

Little piece of advice: DO NOT write your own parser

We didn't follow this advice of course, but please know that there are plenty of excellent parsers that have been written by much smarter people and all you have to do is give it your formal syntax and with a little coercing it will produce objects of your IR. Easy :)

If you're curious about how they work there's plenty of other resources that you can read, but be warned, it gets deep!

The generator

The NPC script that our machine must produce is a python file. And a python file is basically a text file. So our generator is actually a mountain of (organized) if statements that produce strings :)

Here's an excerpt:

def build_reward_part(self, reward, tabs):
    if reward.kind == "money":
        return (
            tabs + f"money = randrange({reward.data['min']}, {reward.data['max']})\n"
            + tabs + "user.money += money\n"
            + tabs + f"{self.get_sound(Sounds.ITEM_REWARD)}\n"
            + tabs + 'user.say(f"You got [FFCC00]{money} pokedollars[-]!")\n'
            + tabs + "user.pause()\n"
        )
    elif reward.kind == "item":
        return (
            tabs + f'user.items["{reward.data["item"]}"] += {reward.data["amount"]}\n'
            + tabs + f"{self.get_sound(Sounds.ITEM_REWARD)}\n"
            + tabs + f'user.say("You got [00AA00]{reward.data["amount"]} {reward["item"].title()}[-]!")\n'
            + tabs + "user.pause()\n"
        )
    elif reward.kind == "pokemon":
        # you get the idea
        ...

The indentation management (through the parameter "tabs") makes the whole thing quite unreadable honestly, I'm sure there's a cleaner way to write this... oh well ¯\(ツ)

Pokémon Politics

After all our hard work we finally had it. A machine that can turn a simple text file - that someone with 0 programming experience could write - into a functionnal Boss NPC Script that can be put into the game.

Ah but there something we didn't talk about: who was I making this machine for ?

The volunteer staff crew maintaining this fan-made Pokémon game, is made of NPC scripters like me, moderators, admins, and more importantly the Content Team. The Content team decides what to put in the game, they design seasonal events, quests, etc. Every new content must have their approval.

My Boss NPC making machine could be useful to other scripters of course, but my hope was that it could be used directly by the Content Team to alleviate some of the work given to NPC scripters.

So I proudly release it, packaging it in a discord bot that you can just send the text file to and get the script back, and the reaction was 🥁 drum rolls 🥁 ... bad?

Yep, that's right, I forgot to talk to the users before excitedly making the product. NPC Scripters didn't see the interest in learning my custom syntax since they already knew how to script, and it got worse: an admin quickly told me to disallow the Content Team from using it, because "it would put too much power in their hands", in other words Politics.

Damn.

Well it was still useful because you know people could design their Boss NPC, submit the text file, the bot could validate it, and then this text file could be sent to a scripter which could use the bot to get the NPC script.

So today we learnt how to make a parser-generator I guess, but the real takeaway here is:
> make sure the users actually want your product before making it!