Skip to content

Extension: Character LLM

On this page we will learn

  • how to run a local large language model (LLM) with Ollama
  • how to give each character its own personality with a custom model
  • how to call the model from our Character class
  • how to handle errors when a character doesn't have a model

Terminology

  • large language model – an AI model, also called an LLM, that has learnt from huge amounts of text and can write replies such as a character's dialogue.
  • Ollama – a program that runs large language models on our own computer, without needing an account or internet connection once a model is downloaded.
  • model parameter – one of the values a language model has learnt, used to measure its size, so a 4b model has 4 billion parameters.
  • server – a program running on a computer that receives requests, such as our game's messages, and sends back replies.
  • system prompt – the instructions that tell a language model who it is, which gives each character its own personality.
  • Modelfile – a text file that tells Ollama how to build a custom model, including its base model, system prompt and settings.
  • temperature – a model setting from 0.0 to 2.0 that controls how creative the replies are, with higher values giving more creative replies.
  • terminal – a window where we type commands for the computer itself, such as ollama, rather than Python code.
  • library – a collection of ready-made code that we install and import so our program can use it, such as ollama.
  • try block – code inside try that Python runs while watching for errors, so a matching except can catch an error instead of the program crashing.

So far, our characters say the same line every time we talk to them. We can make the game more dynamic by using a local large language model (LLM) to create our characters' dialogue.

Set-up

We'll use Ollama to run the LLM on our own computer.

  1. Download and install Ollama.
    • Why: Ollama runs language models locally, so no account or internet connection is needed once a model is downloaded.
    • Expected result: Ollama opens with a chat box.
  2. Choose the gemma3:4b model from the model list in the chat box.
  3. Type "Hello" in the chat box.
    • Why: sending a first message downloads the model.
    • Expected result: after the download finishes, the model replies.

Choosing other models

We can choose other models, and this process will be mostly the same. But the bigger the model, the more memory it needs and the slower it is to reply.

Models are measured by their number of parameters: a 4b model has 4 billion parameters, while a 7b model has 7 billion. We can use Ollama's chat box to try different models and see which one works best for our game.

The list of available models is on the Ollama website.

Ollama works on two levels. There's a chat window we can type in, but it's powered by a server running on our computer. Our game can send requests to this server and get replies from the model.

Create custom models

Rather than using the same model for every character, we'll create a custom model for each one. This gives each character a unique personality and style. All the models use the same base model, but each has a different system prompt: instructions that tell the model who it is.

The Modelfile

Let's start with a model for Nigel. Create a new file in Thonny, add the text below, and save it as nigel.txt in the deepest_dungeon folder.

nigel.txt
FROM gemma3:4b

SYSTEM You are Nigel, a friendly, but grumpy dwarf who specialises in alchemy

PARAMETER temperature 0.2
PARAMETER num_ctx 4096
Code explanation
  • FROM → sets the base model for our custom model. If we chose a different model, we change this to match.
  • SYSTEM → sets the system prompt, which gives the character its personality. We can change this to create different characters.
  • PARAMETER temperature → controls how creative the replies are, from 0.0 to 2.0. Higher values give more creative replies.
  • PARAMETER num_ctx → controls how much text the model can take into account when it replies.

Modelfile reference

More parameters and options are in the Ollama Modelfile documentation.

Create the model

Now we need to open a terminal to run an Ollama command.

  1. In Thonny, go to Tools → Open system shell….
    • Why: ollama is a command for the computer's terminal, not for Python.
    • Expected result: a terminal window opens in the folder of the current file.
  2. Check that the folder in the terminal prompt is deepest_dungeon.
  3. Type the command below and press Enter.
ollama create nigel -f nigel.txt
Code explanation
  • ollama → runs the Ollama program.
  • create → tells Ollama to create a new model.
  • nigel → names the new model. We'll always name a character's model after the character, in lower case.
  • -f nigel.txt → tells Ollama to build the model from the file nigel.txt, which must be in the current folder.

Correct folder

If the terminal isn't in the deepest_dungeon folder, Ollama can't find nigel.txt.

Go back to Ollama's chat window. Our new nigel model should be in the model list. Select it and chat with it to get a feel for how it replies.

Adjust the model

To change the model, edit nigel.txt and run the ollama create command again. Try different system prompts and parameters to create different personalities.

Add the model to the game

Now let's connect the model to our game. We'll use a Python library called ollama to send requests to the Ollama server.

Install the ollama library

  1. In Thonny, go to Tools → Manage packages….
  2. Search for ollama and click Install.
    • Expected result: Thonny shows that ollama is installed.

Store each character's model name

First, each character needs an attribute that stores the name of its model. We always name the model after the character, so Nigel's model is nigel.

Change the dunder init method in character.py as highlighted below.

# character.py

class Character():

    def __init__(self, name):
        # initialises the character object
        self.name = name
        self.model = name.lower()
        self.description = None
        self.conversation = None

    def describe(self):
        # sends a description of the character to the terminal
        print(f"{self.name} is here, {self.description}")

    def talk(self):
        # send conversation to the terminal
        if self.conversation is not None:
            print(f"{self.name}: {self.conversation}")
        else:
            print(f"{self.name} doesn't want to talk to you")

    def hug(self):
        # the character responds to a hug
        print(f"{self.name} doesn't want to hug you")

    def fight(self,item):
        # the character response to a threat
        print(f"{self.name} doesn't want to fight you")
        return True


class Friend(Character):

    def __init__(self, name):
        # initialise the Friend object by calling the character initialise
        super().__init__(name)

    def hug(self):
        # the friend responds to a hug
        print(f"{self.name} hugs you back.")


class Enemy(Character):

    num_of_enemy = 0

    def __init__(self,name):
        # initialise the Enemy object by calling the character initialise
        super().__init__(name)
        self.weakness = None
        Enemy.num_of_enemy += 1

    def fight(self, item):
        # fights enemy with provided item and returns if player survives
        if item == self.weakness:
            print(f"You strike {self.name} down with {item}.")
            Enemy.num_of_enemy -= 1
            return True
        else:
            print(f"{self.name} crushes you. Puny adventurer")
            return False

    def get_num_of_enemy():
        return Enemy.num_of_enemy
Code explanation
  • line 8 → creates the model attribute from the character's name in lower case, so Nigel uses the nigel model.

The chat method

Now let's add a chat method to the Character class. It sends the player's message to the character's model and prints the reply. Add the highlighted code below.

# character.py

import ollama

class Character():

    def __init__(self, name):
        # initialises the character object
        self.name = name
        self.model = name.lower()
        self.description = None
        self.conversation = None

    def describe(self):
        # sends a description of the character to the terminal
        print(f"{self.name} is here, {self.description}")

    def talk(self):
        # send conversation to the terminal
        if self.conversation is not None:
            print(f"{self.name}: {self.conversation}")
        else:
            print(f"{self.name} doesn't want to talk to you")

    def hug(self):
        # the character responds to a hug
        print(f"{self.name} doesn't want to hug you")

    def fight(self,item):
        # the character response to a threat
        print(f"{self.name} doesn't want to fight you")
        return True

    def chat(self, message):
        # use the character's model to have a conversation
        response = ollama.generate(
            model=self.model,
            prompt=message
        )
        print(response['response'])


class Friend(Character):

    def __init__(self, name):
        # initialise the Friend object by calling the character initialise
        super().__init__(name)

    def hug(self):
        # the friend responds to a hug
        print(f"{self.name} hugs you back.")


class Enemy(Character):

    num_of_enemy = 0

    def __init__(self,name):
        # initialise the Enemy object by calling the character initialise
        super().__init__(name)
        self.weakness = None
        Enemy.num_of_enemy += 1

    def fight(self, item):
        # fights enemy with provided item and returns if player survives
        if item == self.weakness:
            print(f"You strike {self.name} down with {item}.")
            Enemy.num_of_enemy -= 1
            return True
        else:
            print(f"{self.name} crushes you. Puny adventurer")
            return False

    def get_num_of_enemy():
        return Enemy.num_of_enemy
Code explanation
  • line 3 → imports the ollama library.
  • line 34 → defines the chat method, which takes the player's message.
  • lines 36–39 → send the message to this character's model and store the model's reply in response.
  • line 40 → prints the text of the reply.

Talk with chat

Now let's change the talk command in main.py so it asks the player what they want to say, and uses chat instead of talk. Make the highlighted changes below.

# main.py

from room import Room
from character import Enemy, Friend
from item import Item

# create rooms
cavern = Room("Cavern")
cavern.description = ("A room so big that the light of your torch doesn’t reach the walls.")

armoury = Room("Armoury")
armoury.description = ("The walls are lined with racks that once held weapons and armour.")

lab = Room("Laboratory")
lab.description = ("A strange odour hangs in a room filled with unknowable contraptions.")

# link rooms
cavern.link_rooms(armoury,"south")
armoury.link_rooms(cavern,"north")
armoury.link_rooms(lab,"east")
lab.link_rooms(armoury,"west")

# create characters
ugine = Enemy("Ugine")
ugine.description = "a huge troll with rotting teeth."
ugine.weakness = "cheese"

nigel = Friend("Nigel")
nigel.description = "a burly dwarf with golden beads woven through his beard."
nigel.conversation = "Well youngun, what are you doing here?"

# add characters to rooms
armoury.character = ugine
lab.character = nigel

# create items
cheese = Item("Cheese")
cheese.description = "super smelly"

chair = Item("Chair")
chair.description = "designed to be sat on"

elmo = Item("Elmo")
elmo.description = "wanting to be tickled"

# add items to rooms
cavern.item = chair
armoury.item = elmo
lab.item = cheese

# initialise variables
running = True
current_room = cavern
backpack = []

# ----- MAIN LOOP -----
while running:
    current_room.describe()

    command = input("> ").lower()

    # move
    if command in ["north", "south", "east", "west"]:
        current_room = current_room.move(command)
        print(f"You travel {command}")
    # talk
    elif command == "talk":
        if current_room.character is not None:
            message = input(f"What do you say to {current_room.character.name}? > ")
            current_room.character.chat(message)
        else:
            print("There is no one here to talk to")
    # hug
    elif command == "hug":
        if current_room.character is not None:
            current_room.character.hug()
        else:
            print("There is no one here to hug")
    # fight
    elif command== "fight":
        if current_room.character is not None:
            weapon = input("What will you fight with? > ").lower()
            available_weapons = []
            for item in backpack:
                available_weapons.append(item.name)
            if weapon in available_weapons:
                if current_room.character.fight(weapon):
                    if isinstance(current_room.character, Enemy):
                        current_room.character = None
                        if Enemy.get_num_of_enemy() == 0:
                            print("You have slain all the enemies. You are victorious!")
                            running = False
                else:
                    running = False
            else:
                print(f"You don't have {weapon}")
                print(f"{current_room.character.name} strikes you down.")
                running = False
        else:
            print("There is no one here to fight")
    # take
    elif command == "take":
        if current_room.item is not None:
            backpack.append(current_room.item)
            print(f"You put {current_room.item.name} into your backpack")
            current_room.item = None
        else:
            print("There is nothing here to take")
    # backpack
    elif command == "backpack":
        if backpack == []:
            print("It is empty")
        else:
            print("You have:")
            for item in backpack:
                print(f"- {item.name.capitalize()}")
    # help
    elif command == "help":
        print("Type which direction you wish to move,")
        print("or use one of these commands:")
        print("- Talk")
        print("- Fight")
        print("- Hug")
        print("- Take")
        print("- Backpack")
    # quit
    elif command == "quit":
        running = False
    # incorrect command
    else:
        print("Enter 'help' to list the commands.")
    input("\nPress <Enter> key to continue")

print("Thank you for playing Deepest Dungeon")

PRIMM

  1. Predict what you think will happen when we talk to Nigel, and when we talk to Ugine. Be specific.
  2. Run the program. Make sure Ollama is running first.
  3. Time to investigate the code. What does each line do?
Code explanation
  • line 69 → asks the player what they want to say to the character, and stores it in message.
  • line 70 → calls the character's chat method with the player's message.

Characters without a model

Talking to Nigel works, but talking to Ugine crashes the game with a ResponseError, because there's no ugine model. Rather than making every character have a model, let's make chat fall back to the old talk method when there isn't one.

Change the chat method in character.py as highlighted below.

# character.py

import ollama

class Character():

    def __init__(self, name):
        # initialises the character object
        self.name = name
        self.model = name.lower()
        self.description = None
        self.conversation = None

    def describe(self):
        # sends a description of the character to the terminal
        print(f"{self.name} is here, {self.description}")

    def talk(self):
        # send conversation to the terminal
        if self.conversation is not None:
            print(f"{self.name}: {self.conversation}")
        else:
            print(f"{self.name} doesn't want to talk to you")

    def hug(self):
        # the character responds to a hug
        print(f"{self.name} doesn't want to hug you")

    def fight(self,item):
        # the character response to a threat
        print(f"{self.name} doesn't want to fight you")
        return True

    def chat(self, message):
        # use the character's model to have a conversation
        try:
            response = ollama.generate(
                model=self.model,
                prompt=message
            )
            print(f"{self.name}: {response['response']}")
        except ollama.ResponseError:
            self.talk()


class Friend(Character):

    def __init__(self, name):
        # initialise the Friend object by calling the character initialise
        super().__init__(name)

    def hug(self):
        # the friend responds to a hug
        print(f"{self.name} hugs you back.")


class Enemy(Character):

    num_of_enemy = 0

    def __init__(self,name):
        # initialise the Enemy object by calling the character initialise
        super().__init__(name)
        self.weakness = None
        Enemy.num_of_enemy += 1

    def fight(self, item):
        # fights enemy with provided item and returns if player survives
        if item == self.weakness:
            print(f"You strike {self.name} down with {item}.")
            Enemy.num_of_enemy -= 1
            return True
        else:
            print(f"{self.name} crushes you. Puny adventurer")
            return False

    def get_num_of_enemy():
        return Enemy.num_of_enemy

PRIMM

  1. Predict what you think will happen when we talk to Nigel, and when we talk to Ugine. Be specific.
  2. Run the program.
  3. Time to investigate the code. What does each line do?
Code explanation
  • line 36 → starts a try block, which runs the code inside it and watches for errors.
  • lines 37–40 → send the message to the character's model, as before.
  • line 41 → prints the reply, starting with the character's name.
  • line 42 → catches the ResponseError that Ollama raises when the model doesn't exist…
  • line 43 → …and runs the character's normal talk method instead.

Ollama must be running

If Ollama isn't running, chat raises a ConnectionError and the game stops. Start Ollama before playing.

Up to you

Now it's our turn. Can you:

  • create models for your other characters, each with its own personality?
  • keep a conversation history for each character, so they remember what the player said earlier?