A Proposed Design for an LLM-Powered Text-Based MMORPG
In this writeup I will specify a few high level components that I think would be crucial for implementing an LLM-powered Text-Based MMORPG. I will identify what I consider to be the key architectural components for building a system that can support an arbitrary number of players interacting within this invented world.
Overview
Let's first consider the architecture at the highest possible level:
To provide some definitions:
- Actor: Any player interacting with the platform. They are capable of performing actions on behalf of their character exclusively through the
Game Interface
. - Game Interface: A chat interface through which each player can manipulate their character (and by extent the
Entities
in the game). - Entities: A database containing an entry for every
Entity
present in the game. - Entity: Any distinctly identifiable object in the game world. A player's character is an entity. A tool is an entity. An area in which events take place is an entity.
Gameplay Loop
The core gameplay loop consists of an actor performing an action, then the game updating its state in accordance with the actors action.
In this diagram we can see that a single player message will trigger an update on all nearby entities. Let's explore an example to see how this would work.
Player 1: Pick up the shovel
player_entity: Entity(id=1, name="thorne", state="")
nearby_entities: [Entity(name="shovel"), Entity(name="dirt patch"), Entity(name="treasure", state="buried in the dirt patch)]
Now we will update each entity:
player_effect = message_to_entity_effect("Player 1: pick up the shovel", player_entity, nearby_entities) # passing nearby entities is necessary to verify that a shovel is available
update_entity(player_effect) # where this is the updated state of the player
for entity in nearby_entities:
accessories = [player_entity] + nearby_entities.pop(entity) # psuedocode for ignoring the current entity
result = message_to_entity_effect("Player 1: pick up the shovel", entity, accessories)
update_entity(result)
respond_to_player()
After updates we should see the resulting states:
player_entity: Entity(id=1, name="thorne", state="holding Entity(id=id, name='shovel'", events=['picked up a shovel']))
nearby_entities: [Entity(name="shovel", state="being held by Entity(id=1, name='thorne'), Entity(name="dirt patch"), Entity(name="treasure", state="buried in the dirt patch)]
Game Response: Thorne picks up the shovel nearby
Now lets dig.
Player 1: Dig a hole in the ground
Skipping the updates...
player_entity: Entity(id=1, name="thorne", state="holding Entity(id=id, name='shovel'. Just dug a hold in the ground. A bit tired", events=['dug a hole in the ground', 'picked up a shovel']))
nearby_entities: [Entity(name="shovel", state="being held by Entity(id=1, name='thorne'), Entity(name="dirt patch", state="a hole is dug out of it", events=["Entity(id=1) dug a hole"]), Entity(name="treasure", state="revealed but partially buried still in the dirt patch")]
Game Response: Thorne digs a hole in the ground and reveals the edge of what looks to be a treasure chest!
We can see how the rest might continue.
Comments on the Gameplay Loop
You might be concerned that I am skipping over a lot of implementation details. How is the state updated? How are we going to keep track of the shovel if the player moves away? My answer, LLMs. Each time a player performs an action we just ask the LLM to generate a reasonable new state and description for the event that just transpired. I am skipping over the logic involved with creating/deleting entities though.
Alternative Approaches
It might be non-useful to keep track of history on a per-entity basis. Alternatively we could consider an additional table, Locations
, to store this sort of archival information:
This has the benefit of not needing to do some sort of distance measurement from the player entity each time they perform an action. It can also help appropriately scope the entities that might be affected by a player action. Mentally modify the gameplay loop where applicable.
Conclusion
While the architecture I have proposed is quite simple, I think it is just as powerful. LLMs enable us to hold highly unstructured data, and convert it to meaningful modifications to entity states. In a follow up article I plan to address the actual programming architecture one would need to build out this system.
As always, feel free to contact me at
contact [at] thornewolf [dot] com