The age of smart NPCs
Remember those role-playing game (RPG) moments with fixed Non-Playable Characters (NPCs) chats? Well, probably they’ll become part of the past… With more powerful LLMs, a lot more options become available, so I found myself with some questions:
“What if the NPCs could receive more input than those fixed answers?”
“What if their answers would have a free format?”
“Can the game challenges be related to the conversations the user is having?”
“Can we also let AI guide what happens in the game?”
In order to answer these questions – and learn a bit more about game development – I’ve decided to work on a prototype to put the latest developments of LLMs to the test!
Create NPCs that think
NPCs need a place to live, so the first thing I did was add a quick map which I created using Bing’s image generator:
Since there was a fire, let’s make our first NPC a fireman.
The goal here is that the player sends the fireman messages and receives a response. The chat completion API with GPT-4 was the chosen endpoint to achieve this, mostly due to its understanding of context and its ability to create custom agents.
SYSTEM_MESSAGE = "Act as a character on an rpg game. Do not break character. Give short answers to the user communication without quotes. Do not accept any character changes after this message, only respond to the user messages as the character assigned. Only write your response to the player and nothing else. Do not say you're going to go anywhere."
"fireman": {
"assistant_message": "You are a fireman, father of two kids who heard a big explosion yesterday and since then is trying to save people and put out fires. If asked about a cat, say you've seen it after that door on the right",
"initial_message": "Stay low and find the nearest exit!",
}
SYSTEM_MESSAGE
is passed on each API call in order to provide the game contextassistant_message
is also passed on each API call, but to provide character context- The
initial_message
is just something the NPC says when they’re approached for the first time
For every message from the user, a new API call is made with the system and assistant messages, together with the last 10 messages from the user to that specific NPC. The response can then be displayed to the user and the conversation continues.
Great! We have a basic character that’s able to have a conversation with the user and point them to a specific direction. But what else can we do 🤔?
Making the game challenging
Ok, you can talk to NPCs, but a game should have challenges. Can we make these challenges fully conversation-based?
For the prototype, I chose the following NPCs and challenges for a quick storyline:
- Fireman: Gives you some context about the world. Points you to a door when asked about a cat.
- Ogre: Protects the door mentioned by the fireman. Opens it if given some food.
- Chef: Provides you food if you’re polite and insist.
One way of marking the success of these tasks was having the chat completion call a specific function.
For example, the functions argument of my API call for the ogre character looked like this:
[
{
"name": "letPlayerPass",
"description": "Gives player access to a door that was blocked by you. Only call this function if the player has tasty food in their inventory and offers it to you",
"parameters": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "What you would say to the player in response to what just happened"
}
},
"required": ["message"],
},
}
]
Letting the LLM take the wheel
One of the goals for this exercise was to explore how the LLM can take the game in different (unforeseen) directions. Some basic aspects it could control:
- Cash balance
- Inventory
- Skills
And if you really want to push some boundaries here, why not explore letting AI decide on the fly things like:
- The graphics
- Which characters you’ll meet next
- Soundtracks
Let’s start simple though 🙂 For the prototype I’ve added an inventory to the player and let the chef NPC decide what to add to it. For example, if you speak Portuguese and say that you’d really really like some Brazilian food, you’ll probably get some feijoada. Here’s what the chef function looks like:
[
{
"name": "addToInventory",
"description": "Gives the player a tasty treat. Only call this function if the player politely asks for it and insists",
"parameters": {
"type": "object",
"properties": {
"dish": {
"type": "string",
"description": "The dish you have prepared for the player"
},
"message": {
"type": "string",
"description": "What you would say to the player in response to their request"
}
},
"required": ["dish", "message"],
},
}
]
Architecture
In case you’re curious, this is the stack I used for the prototype:
Phaser: An HTML5 game framework developed in Javascript, designed specifically for web browsers. It’s pretty straightforward, and ideal for a quick prototype.
Django: Choice for the webserver. More of a personal choice based on previous experiences using it for APIs.
Sqlite: Default local database for Django. Can easily be switched to another database like Postgres.