AI Is Missing - Implementing a Multi-Agent Role Playing Game
As promised, this post will break down how I implemented a role playing game where several independent AI agents take the role of players (rather than the game master) using SemanticKernel and managing the game state through the ChatHistory object. I’m using the popular Alice is Missing role playing game published by Hunters Entertainment, which is a text-based system that focuses on interactions between players and doesn’t require a game master.
The Game
Alice is Missing is a role-playing game where players act out their character’s perspectives with one another through text. This makes it quite unique among role-playing games, but also a prime candidate for something like a text-based generative AI system to work with!
The concept of the game is each player plays as a friend of Alice, a recent high school grad who’s gone missing from your small town of Silent Falls. Players generate their characters using tools provided by the game system and establish a background, personality, secret, drive, and relationships with other characters in the game. The game setup also allows players to frame the context of the potential suspects and locations that can be visited before the role-playing starts.
The game is also unique (although less so) in that it doesn’t require a game master, a traditional role that is ususally filled by one of the players at the table. The game master is usually responsible for the setting and non-player characters in a role-playing game; they’re the ones that decide how the game reacts to each player’s actions. In a GM-less RPG however, this is usually replaced by some sort of randomizer. In Alice is Missing’s case, there’s a series of timed events that occur throughout the real-time game which give individual players specific prompts and information to change the direction the game is heading in.
Once the characters, suspects, and locations are set up, gameplay follows by setting a 90 minute timer and having all the players log into a shared chat application, where most of the interactions will happen.
Character Creation
The first step of the game is to establish characters. In person, this is done by choosing a character card that has a few things on it - your name, relationship to Alice, some worldbuilding questions, a randomly assigned “drive” or directive as to how your character should act, and a secret that only your character knows.
In my generative AI-driven version of the game, I don’t stray away from this too much. In a separate process without any prior chat history, here’s the prompt I use:
System: You are a player in a role playing game. Your name is {name}.
You are {persona}. Your personality can be described as {traits}. A few days ago, Alice stopped responding to messages and calls. You haven't heard from her since.
Answer the following questions in second person, using You instead of I. Answer with one paragraph.
User: {worldbuilding prompt}
Assistant:
--
User: {secret prompt}
Assistant:
After each of the user prompts I make a call to GetChatMessageContentAsync
and record the response. You’ll notice I ask the model to generate a response in second person; this is specifically so that I can use the response verbatim when I make the character’s system prompt later. With that output, a character’s constructed system prompt ends up looking something like this:
System: You are a player in a role playing game. Your name is {Jack}. You are 19 years old.
You are {Alice's older brother}.
Your personality can be described as {Rational and Interesting}.
Your background: {You and Alice have faced significant challenges at home due to the constant pressure to meet the high expectations set by your parents. With their relentless demands for academic excellence and involvement in extracurricular activities, you both found it increasingly difficult to carve out your own identities or pursue your passions. This environment fostered a sense of stress and uncertainty, leading to communication breakdowns, especially in the case of Alice, who seemed to struggle silently under the weight of it all. As a result, the bond you once shared has been strained, making it harder for you to connect and understand her struggles, particularly in these last few years.}
There's a secret only you know: {You and Alice got into a heated argument over her decision to drop out of school. You believed that education was crucial for her future and that she was throwing away opportunities for a fleeting desire to chase her dreams without a clear plan. You tried to rationalize your concerns, emphasizing the importance of stability and responsibility, but Alice felt stifled by your expectations and pushed back fiercely, claiming that you didn’t understand her passion. The tension escalated, and you both said things you regretted, leaving the air heavy with unresolved feelings just before she stopped responding to you.} Try not to tell anyone else.
{You fear the worst for Alice. Jump to conclusions, make conspiracy theories, and blame yourself for her disappearance.}
Prefix all your responses with Jack:
Note that in my prompt examples in this post, I’ll put text that was inserted using a prompt template within { curly braces }. Those braces aren’t actually in the generated prompt, I just wanted to give a visual cue that the values were injected.
You can see how the answers to the background worldbuilding and personal secret slot into the player system prompt, along with the other randomly selected elements of name, relationship to Alice, personality, and drive.
The Player System Prompt
This player system prompt is important, as it sets the context for how we want this particular player to respond. In code, I maintain this through a separate Character
class that not only contains the information needed to construct this system prompt, but also provides a method, Respond
, to specifically prompt this player to generate a response.
Behind the scenes, what Respond
does is take in a ChatHistory object and manipulate a local copy of it. As illustrated in my earlier post:
It’s through this careful manipulation of the global ChatHistory object that gets passed between players that we enable each Character
to respond using their unique perspective.
Introductions
In the game, players introduce their characters to each other with their pronouns, relationship to Alice, physical description, and a brief summary of their background. Not much of a stretch to have our newly created characters do that as well:
[character system prompt]
[game intro explaining Silent Falls and Alice's disappearance]
User: Introduce yourself by stating your name and pronouns, your relationship to Alice, a physical description of your character, and a summary of your background.
Jack: My name is Jack, and I use he/him pronouns. I'm Alice's older brother, and I have always tried to look out for her, despite the challenges we've faced at home. I stand about 6 feet tall with a lean build, having inherited my mother's dark hair and my father's sharp blue eyes. I often wear a denim jacket over hoodies, paired with comfortable jeans—practical but still holding onto my interests in music and arts.
Growing up, both Alice and I were under immense pressure from our parents to excel academically and participate in various activities. This environment, while it pushed us to achieve, also stifled our personal passions and identities. For Alice, I sensed that the weight of these expectations was particularly heavy, leading to tension between us as she struggled silently with her desires. Recently, we had a fierce argument regarding her decision to drop out of school, exacerbating the distance that had already formed in our relationship. Now, with Alice disappearing without a word, I can't shake off the fear that something terrible might have happened to her, and I often blame myself for not understanding her struggles better.
You can already see how Jack’s player system prompt is shaping his replies.
Relationships
After introductions, each player is asked to pick another player and establish a relationship with them. These relationship queues generally come from the character cards, and invite you to make up reasons you like/dislike the other characters at the table.
In my implementation, it’s a simple matter of randomly selecting two other characters and using those relationship prompts from the game.
User: {Dakota}, in one paragraph describe your relationship with {Charlie} using the prompt '{Alice has forgiven you for something, but I haven't}'.
Dakota: Charlie, there was a time when you tried to dismiss Alice's concerns about her mental health, brushing them off as just her being too analytical or overthinking things. Whether you realized it or not, it hurt her deeply, and I can't shake the feeling that your casual attitude added to her isolation. Alice forgives you, believing that everyone gets caught up in their own lives sometimes, but I can’t let go of the fact that you didn’t take her struggles seriously when she needed support the most. It drives me to stay hyper-aware of the people around me, especially when it comes to Alice, and I just wish you could understand the impact your words can have."
It was really interesting seeing some of the replies that the bots came up with, and how it drew not only from their own context but the character descriptions of the players they were referencing.
I also have the character classes store this information, and optionally add it to their future prompts as necessary - specifically if they’re responding to a message from another character that they have a relationship with, I’ll remind them of that relationship before they craft their reply. This has made for some interesting dialog back and forth between players.
Suspects and Locations
The next step in the game setup is to discuss as a group why each of the potential suspects and locations are suspicious in the context of Alice’s disappearance. The in-person game has each player pick one of the 5 options for each and narrate a quick bit about why they feel that particular person or location is suspicious, which other players can then take with them into the game.
Now that we have our generative AI character system prompts constructed, it’s not hard to have them make up stories about suspects or locations!
User: {Jack}, in one paragraph explain why {the lighthouse on the Howling Sea Cliffs} is suspicious in relation to Alice's disappearance.
Jack: The lighthouse on the Howling Sea cliffs has always been a focal point of local legends, often linked to mysterious happenings in Silent Falls, and its isolation makes it a suspicious location in relation to Alice's disappearance. Recently, rumors have circulated about strange lights and eerie noises emanating from the lighthouse at odd hours, coinciding with the time Alice went missing. Given her adventurous spirit, it wouldn't be far-fetched to think she might have ventured there out of curiosity, especially since she loved exploring hidden corners of our town. The ominous aura surrounding the lighthouse, coupled with the unsettling tales of past visitors who have encountered inexplicable events, heightens my suspicion that something sinister may have happened to Alice in that very spot, making it a critical lead in uncovering the mystery of her absence.
These descriptions are included in the game’s chat history and are referenced by the bots as they play the game.
Game Constraints as Prompts
There’s a few rules when playing Alice is Missing. Characters aren’t allowed to meet at locations in real life - you can choose to travel to one of the five locations in the game, but you’re not allowed to be there at the same time as another player (or if you are, you can’t meet up). This is primarily to keep all the communication happening in the group chat. In addition, you’re not allowed to actually find Alice until you trigger a timed event that says you have.
I also wanted to constrain the bots to respond as players would through group text, so I added that and the other game rules to the game start system prompt.
System: The game is now starting.
You should respond in short sentences as though writing a text message, no more than 160 characters at a time.
Two players can't be in the same place at the same time, and you cannot meet in person.
The locations you can search include the Silent Falls train station, Kalisto Rivers State Park, the Dripping Dagger nightclub, the old barn on Cambridge Street, or the lighthouse on the Howling Sea cliffs.
Only one player can search a location at a time.
You won't find Alice unless you're explicitly told you found her.
Game Mechanics as Tools
One of the other things you can do in the game is actually search a location. If you travel to a game location unprompted (i.e. not because of a timed event), there’s a deck of cards you can draw from to give you a prompt as to what you found at that location. I was able to emulate this function by adding a tool to the SemanticKernel ChatCompletionService
:
public class SearchFunction
{
[KernelFunction("search_location")]
[Description("Returns what was found while searching the barn, nightclub, park, train station, or lighthouse")]
public string SearchLocation()
{
var index = _random.Next(foundItems.Count);
var result = foundItems.ElementAt(index);
return result;
}
}
//associate funciton call with chat completion service
var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion("gpt-4o-mini", key);
builder.Plugins.AddFromType<SearchFunction>("search");
var _kernel = builder.Build();
//when generating a response
var response = await _chatCompletionService.GetChatMessageContentAsync(localChatHistory, new PromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
}, _kernel);
With a bit of configuration, I was able to provide some code that the player bots would call into once they arrived and search at one of the game locations!
Dakota: Exactly, Julia! Our determination is stronger together. I’m heading to the lighthouse now, and I'll keep my eyes peeled for any signs. Let’s do this! 🕵️♀️
----
Julia: Awesome, Dakota! Stay safe and trust your instincts. I believe we’ll find Alice together! Let us know if you find anything! 💪💖
----
[Tool was called here: returned "a sketchy white van"]
Dakota: Just arrived at the lighthouse. It’s eerie here. 🏚️ I spotted a sketchy white van parked nearby. No idea who it belongs to. Should I check it out? 🚨
----
Julia: Be cautious, Dakota! If it feels risky, it might be better to keep your distance and observe first. Just stay safe and let us know what happens! 😟💖
As an interesting aside, every other game the bots seem to be heavy into the emojis - once one bot starts with it, almost all of them continue the trend. And if no one starts it, the bots stay away from it!
Let’s Play!
This concludes the set up of the game. This is where players in the in-person game would take a bit of a break, get comfortable, and then start a 90 minute timer, sending messages to each other as their characters through a shared text interface like Discord or Slack or even a group text.
Each player is also given a random assortment of cards with a timer cue on the back of them. There’s an assortment of events that fire of at the 80, 70, 60, etc. minute mark of the game that are designed to inject twists and plot points into the story, adding an element of surprise and interest as you play.
Both of these things can be emulated by our application. The main game loop looks something like this:
while(elapsed < TimeSpan.FromMinutes(90))
{
//we randomize this list to give different characters a chance to respond first
var listOfCharacters = characters.OrderBy(c => rand.Next());
var nextEvent = events.First(); //events is an ordered dictionary of <TimeSpan, Func<Character, ChatHistory>>
if (nextEvent.Key > elapsed)
{
var randomCharacter = listOfCharacters.First();
//this generates a prompt based on a random event for the given trigger time
var prompt = nextEvent.Value(randomCharacter, gameHistory);
var response = await randomCharacter.Respond(gameHistory, prompt);
elapsed += ProcessResponse(gameHistory, response);
events.Remove(nextEvent);
}
else
{
foreach (var character in listOfCharacters)
{
if (character.WantsToRespond(gameHistory))
{
var response = await randomCharacter.Respond(gameHistory);
elapsed += ProcessResponse(gameHistory, response);
break;
}
}
}
}
While the game is still going (based on time elapsed), check to see if an event has triggered; if it has, pick a character to resolve it. Otherwise, ask each character (in a random order each cycle) if they’d like to respond, and if so, collect their response.
The ProcessResponse
method does a few things, including adding the character’s response to the ChatHistory
object, printing it to the screen, and determining an artificial amount of time that has elapsed. I do this to emulate the idea that these characters are typing these responses to you - if we proceed at the speed that the gpt-4o-mini model would like, you’d be getting a lot more messages than you would in a normal game of Alice is Missing.
Here’s an example game run with an event:
Julia: We should definitely look for anything that doesn’t belong at the barn, like personal items or signs of struggle. Alice is creative, so maybe her art supplies could give us a clue!
----
Evan: Great point! If we find any of her stuff, that might mean she's been there recently. Or maybe whatever's going on might be tied to her somehow.
Do you think anyone in town has been acting out of character lately? Like, has anyone been hanging around Alice more than usual that could give us a clue?
----
[Event Trigger: {You remember Alice finding out about something dark in {Mr Halvert}'s past. What bad or scary thing about {Mr. Halvert} did Alice uncover? Tell the other players.}]
Evan: You know what I just remembered? Alice found out some pretty dark stuff about Mr. Halvert's past. There were whispers he might've been involved in some shady local legends linked to missing people!
It gave her the creeps, and she was always cautious around him after that. Do you think it could be connected to her disappearance? I can’t shake the feeling that he might know something he isn’t telling us. What do you think?
----
Julia: That is really concerning, Evan! Alice was always cautious and aware of her surroundings, especially with people like Mr. Halvert. I can’t imagine her confronting him, though!
She wouldn’t put herself in danger, but if she felt uneasy about him, it's possible she sensed something off. Maybe he has been seeing her as a threat to his secrets. We should definitely keep an eye on him while looking for her!
----
Dakota: I'm really worried about that too. Mr. Halvert's interest in local legends and his behavior around Alice is definitely suspicious. We should be careful.
If he knows anything about her disappearance, we need to find out. After checking the barn, I might want to keep an eye on him.
Next Steps
At this point I have a fully automated game of Alice is Missing. While that is interesting, I’d like to change it so that a player (or players) can create their own characters and interact with the bot characters. That’ll involve moving to some sort of user interface more expressive than a console app and likely some web socket communication. I’ll post here as I make progress!