I enjoy role-playing games greatly, and have been getting into the wonderful Baldur’s Gate 3. It’s a stellar example of technical and artistic craftsmanship, and deserving of all the good reviews garnered for its gameplay and writing.

What I enjoyed the most are the extensive dialogue trees in Baldur’s Gate. Originally, a mainstay of old 90s CRPGs, the mechanic has popped up in modern games like Mass Effect, episodic TellTale Games, and Disco Elysium for deep narratives and NPC interaction.

I’ll be detailing a simple implementation of a stateless dialogue tree running in the command line. Stateless implies that no conditional quest-related flags are used to trigger the entry point to a particular conversation or branch. This can be a feature worth extending the provided code on.

Class: Conversation Manager

The Conversation Manager class processes a dictionary of conversations, which are each dictionaries holding all the dialogue constituting a conversation.

from typing import Dict
from dialogue import *

class ConversationManager:
    conversations: Dict[str, Dict[int, Dialogue]]
    conversation_id: str

    def __init__(self, conversations: Dict[str, Dict[int, Dialogue]]) -> None:
        self.conversations = conversations
        for conversation in self.conversations:
            for dialogue in self.conversations[conversation]:
                self.conversations[conversation][dialogue].instancer = self

    def play(self, conversation: str) -> None:
        self.conversation_id = conversation
        self.conversations[self.conversation_id][0].play()

Class: Dialogue

The Dialogue class held in the dictionary of conversations are dependent on the existence of the calling Conversation Manager. Each Dialogue instance has an assigned speaker, an action prompt, and the content of dialogue.

from typing import List

class Dialogue:
    speaker: str
    prompt: str
    content: str
    choices: List

    instancer = None

    def __init__(self, speaker: str, prompt: str, content: str, choices: List[int]) -> None:
        self.speaker = speaker
        self.prompt = prompt
        self.content = content
        self.choices = choices

    def play(self) -> None:
        print('{}: {}'.format(self.speaker, self.content))

        i: int = 0
        for choice in self.choices:
            print('[{}] {}'.format(str(i + 1), self.instancer.conversations[self.instancer.conversation_id][choice].prompt))
            i += 1

        if len(self.choices) > 1:
            choice: int = int(input('?'))
            choice = max(0, min(choice, len(self.choices)))
            choice -= 1
        else:
            choice: str = input('?')
            choice: int = 0

        if len(self.choices) > 0:
            self.instancer.conversations[self.instancer.conversation_id][self.choices[choice]].play()
        else:
            self.instancer.conversation_id = ''

Entry Point

We simply pass the dialogue tree as a conversation to the Conversation Manager. We can pass multiple conversations in this manner, referencing which one to play after the fact.

from conversation_manager import *

def main():
    conversation_manager = ConversationManager(
        {
            'conversation_0': {
                0: Dialogue('Narrator', 'START >>', 'Before you is a computer. What do you do?', [1, 2, 3]),
                1: Dialogue('Narrator', 'Turn on the computer.', 'It did not respond and stays inert.', [4]),
                2: Dialogue('Narrator', 'Check if the lid is warm.', 'It seems cold, like it has not been turned on in some time.', [4]),
                3: Dialogue('Narrator', 'Call tech support on your phone.', 'The line is busy and the call ends.', [4]),
                4: Dialogue('Narrator', 'CONTINUE >>', 'There is nothing to be done.', [])
            }
        }
    )

    conversation_manager.play('conversation_0')

if __name__ == '__main__':
    main()

<
Previous Post
Neural Network Implementation I
>
Next Post
Darkhaan