How to Generate Random RPG Character Names with an LLM

How to Generate Random RPG Character Names with an LLM

I recently led a workshop on developing LLM-based RPG game tools. One tool was designed to generate random character names based on their in-game race (Elf, Human, Dwarf...). However, we quickly noticed that we were all getting the same names (on different machines) even when rerunning the program.

In this blog post, we'll explore how to improve name generation quality in terms of originality and achieve more random results. We'll use small (and very small) LLMs for these experiments:

don't forget to install them

But first, let's take a look at my name generation program.

Generating a Character Name

This program is a character name generator (NPC) for RPGs like D&D:

  1. Structure and configuration:
  • Defines a Character structure

  • Connects to Ollama

  1. AI Communication:
  • Sends instructions to generate character names

  • Specifies JSON schema for structured response

  • Configures parameters like temperature and repetition penalties

  1. Generation:
  • Asks AI to generate a name for a specific type (Human, Elf, or Dwarf)

  • Receives JSON response

  • Converts JSON to Character object

  • Displays generated name and type

Here's the Go generation code:

type Character struct {
    Name string `json:"name"`
    Kind string `json:"kind"`
}

func main() {

    ctx := context.Background()

    ollamaUrl := os.Getenv("OLLAMA_HOST")
    model := os.Getenv("LLM")

    fmt.Println("🌍", ollamaUrl, "📕", model)

    client, err := api.ClientFromEnvironment()
    if err != nil {
        log.Fatal("😡:", err)
    }

    systemInstructions := `You are an expert NPC generator for games like D&D. 
    You have freedom to be creative to get the best possible output.
    `
    // define schema for a structured output
    // ref: https://ollama.com/blog/structured-outputs
    schema := map[string]any{
        "type": "object",
        "properties": map[string]any{
            "name": map[string]any{
                "type": "string",
            },
            "kind": map[string]any{
                "type": "string",
            },
        },
        "required": []string{"name", "kind"},
    }

    jsonModel, err := json.Marshal(schema)
    if err != nil {
        log.Fatalln("😡", err)
    }

    //kind := "Dwarf"
    kind := "Human"
    //kind := "Elf"
    userContent := fmt.Sprintf("Generate a random name for an %s (kind always equals %s).", kind, kind)

    // Prompt construction
    messages := []api.Message{
        {Role: "system", Content: systemInstructions},
        {Role: "user", Content: userContent},
    }

    //stream := true
    noStream := false

    req := &api.ChatRequest{
        Model:    model,
        Messages: messages,
        Options: map[string]interface{}{
            "temperature":    0.0,
            "repeat_last_n":  2,
            "repeat_penalty": 2.2,
            "top_k":          10,
            "top_p":          0.5,
        },
        Format: json.RawMessage(jsonModel),
        Stream: &noStream,
    }

    generateName := func() (string, error) {
        jsonResult := ""
        respFunc := func(resp api.ChatResponse) error {
            jsonResult = resp.Message.Content
            return nil
        }
        // Start the chat completion
        err := client.Chat(ctx, req, respFunc)
        if err != nil {
            return jsonResult, err
        }
        return jsonResult, nil
    }
    // call talkToLLM 5 times
    jsonStr, err := generateName()
    if err != nil {
        log.Fatal("😡:", err)
    }
    character := Character{}

    err = json.Unmarshal([]byte(jsonStr), &character)
    if err != nil {
        log.Fatal("😡:", err)
    }

    fmt.Println(character.Name, character.Kind)

}

Running the program multiple times using qwen2.5:0.5b:

OLLAMA_HOST=http://localhost:11434 \
LLM=qwen2.5:0.5b \
go run main.go

Each time, I get:

Ethan Human

Trying with a slightly larger version qwen2.5:1.5b:

OLLAMA_HOST=http://localhost:11434 \
LLM=qwen2.5:1.5b \
go run main.go

I get:

Aurora Kind Aurora

Running it multiple times yields the same name. Similar behavior occurs with qwen2.5:3b. So model size doesn't seem to significantly impact generation randomness (though it might affect name originality).

Let's see how we could influence the LLM's behavior.

Generating a Character Name: Playing with Options

Let's modify the LLM parameters to improve generation randomness:

Options: map[string]interface{}{
    "temperature":    1.7,
    "repeat_last_n":  2,
    "repeat_penalty": 2.2,
    "top_k":          10,
    "top_p":          0.9,
},

I increased temperature to enhance creativity and adjusted top_p, which maintains coherence, slightly upward to allow for more creativity.

Testing again with qwen2.5:0.5b:

OLLAMA_HOST=http://localhost:11434 \
LLM=qwen2.5:0.5b \
go run main.go

Each run produced a new name:

Maggie Brown Human
Marius Human
Rexa Human

Then with qwen2.5:3b:

Ethan Thorne Human
Eldric Flintwhistle Human
Ethan Valor Human
Ethan Renwick Human

Results appear more original and inventive with the larger model. While I could improve my prompt to help the LLM's generation, there might be models better trained for such tasks.

Let's look at nemotron-mini:4b

Let's Change Models Again

With identical source code and parameters, let's examine nemotron-mini, which appears to have some "roleplay" capabilities and the ability to embody fictional characters.

OLLAMA_HOST=http://localhost:11434 \
LLM=nemotron-mini:4b \
go run main.go

Running the program multiple times yields:

Aurelius Silvermoon Human
Valeira Human
Aurelia Human
Elara Human
Erin Human

The results look promising. Curious about which model between qwen2.5 and nemotron-mini is more "skilled," I modified my program to automatically generate names multiple times and save the results.

Batch Name Generation

Code modification (calling the generation function 15 times):

characters := []Character{}
for i := 0; i < 15; i++ {
    // Generate a random name
    jsonStr, err := generateName()
    if err != nil {
        log.Fatal("😡:", err)
    }
    character := Character{}

    err = json.Unmarshal([]byte(jsonStr), &character)
    if err != nil {
        log.Fatal("😡:", err)
    }
    fmt.Println(character.Name, character.Kind)

    characters = append(characters, character)
}

// Create a Markdown table
markdownTable := "| Index | Name     | Kind       |\n"
markdownTable += "|------|----------|------------|\n"

// Add rows to the Markdown table
for idx, character := range characters {
    markdownTable += fmt.Sprintf("| %d   | %s      | %s       |\n", idx+1, character.Name, character.Kind)
}

// Write the Markdown table to a file
err = os.WriteFile("./characters."+kind+".md", []byte(markdownTable), 0644)
if err != nil {
    log.Fatal("😡:", err)
}

Batch Generation Results

qwen2.5:0.5b

OLLAMA_HOST=http://localhost:11434 \
LLM=qwen2.5:0.5b \
go run main.go
IndexNameKind
1Dwarven KingDwarf
2GandalfDwarf
3Elvenhaldwarf
4Krymdwarf
5GornathDwarf
6DawnDwarf
7BrambleDwarf
8Valkyrie GnomeDwarf
9Boradric the DwarfDwarf
10Gorilla DwarfDwarf
11Dwarven KnightDwarf
12Elven ElbowDwarf
13BaldurDwarf
14Dwellerdwarf
15Elder FrostbiteDwarf

qwen2.5:1.5b

OLLAMA_HOST=http://localhost:11434 \
LLM=qwen2.5:1.5b \
go run main.go
IndexNameKind
1Orcus StonefurDwarf
2OrelythDwarf
3MithrandirDwarf
4GrimmhammerDwarf
5OlivierDwarf
6DwarfintheGreenThicketDwarf
7ThaurinDwarf
8ThranduinDwarf
9ThranduilDwarf
10LorwynthAuril
11ThrenadelNimble
12GlenvorDwarf
13MithrilDwarf
14Rudric the BlackhammerDwarf
15Mikaela'vaarDwarf

qwen2.5:3b

OLLAMA_HOST=http://localhost:11434 \
LLM=qwen2.5:3b \
go run main.go
IndexNameKind
1Grog ThunderjawDwarf
2Grommek StouthammerDwarf
3Karngrim StonehammerDwarf
4Thorgar StonehammerDwarf
5Korvath IronclawDwarf
6Grolgar BlackclawDwarf
7GromthunderblastDwarf
8Krogsharn BlackfrostDwarf
9Grimstone StouthammerDwarf
10Kromberg IronfootDwarf
11Grommash BoulderjawDwarf
12Grondulf the GrimDwarf
13Grundgrond the BoulderbornDwarf
14KraggthorDwarf
15Gorogthar the GrimDwarf

nemotron-mini:4b

OLLAMA_HOST=http://localhost:11434 \
LLM=nemotron-mini \
go run main.go
IndexNameKind
1Grinchbearddwarf
2Gimli the StoutDwarf
3GriphstoneDwarf
4Thrall the StoutDwarf
5Tristram StoutheartDwarf
6Gimli OakheartDwarf
7Thorin StonefootDwarf
8Gimli OakbeardDwarf
9Gimli IronhideDwarf
10OakbeardDwarf
11Grimbeard the Stoutdwarf
12GrimhammerDwarf
13Thor's HammerDwarf
14IronheartDwarf
15Oakbeard OakthunderDwarf

After these initial tests, I find qwen2.5:1.5b offers the best results in terms of both randomness and originality, though this is subjective.

Key takeaways:

  • Adjusting model parameters easily yields random name lists

  • Model choice affects name originality

  • To prevent repetition, we could store previous name generations in the message list (conversation memory) and instruct the LLM to avoid generating existing names

Let's Build a Better Prompt

I added more detailed generation instructions to the model:

generationInstructions := `
## Suggested Generation Rules

For generating consistent names, here are some guidelines:

### Dwarves
- Favor hard consonants (k, t, d, g)
- Use short, punchy sounds
- Incorporate references to metals, stones, forging
- Clan names often hyphenated or compound words
- Common suffixes: -in, -or, -ar, -im

### Elves
- Favor fluid consonants (l, n, r)
- Use many vowels
- Incorporate nature and star references
- Names typically long and melodious
- Common prefixes: El-, Cel-, Gal-
- Common suffixes: -il, -iel, -or, -ion

### Humans
- Greater variety of sounds
- Mix of short and long names
- Can borrow elements from other races
- Family names often descriptive or location-based
- Common suffixes: -or, -wyn, -iel
- Common prefixes: Theo-, El-, Ar-    

## Usage Notes
Names can be modified or combined to create new variations while maintaining the essence of each race.

### Pattern Examples
- Dwarf: [Hard Consonant] + [Short Vowel] + [Hard Consonant] + [Suffix]
- Elf: [Nature Word] + [Fluid Consonant] + [Long Vowel] + [Melodic Ending]
- Human: [Strong Consonant] + [Vowel] + [Cultural Suffix]

### Cultural Considerations
- Dwarf names often reflect their crafts or achievements
- Elf names might change throughout their long lives
- Human names vary by region and social status
`

Then, I added these new instructions to the LLM message list:

// Prompt construction
messages := []api.Message{
    {Role: "system", Content: systemInstructions},
    {Role: "system", Content: generationInstructions},
    {Role: "user", Content: userContent},
}

Let's run batch name generation with our 4 models:

New Batch Generation Results

qwen2.5:0.5b

IndexNameKind
1Dwarven ValtorDwarf
2ElmaronDwarf
3DwarvixDwarf
4Khan-El-TanarDwarf
5GryphDwarf
6Karlknight
7GryphonDwarf
8FernillaDwarf
9Kindenkind
10Darth KaelDwarf
11KaelinorDwarf
12EonwindDwarf
13Eon-Dwarf
14El'karthdwarf
15GaelionDwarf

qwen2.5:1.5b

IndexNameKind
1Threnadinor SteelhammerDwarf
2Thaklinor DurinDwarf
3RukhkarDwarf
4ThornkinDwarf
5ThakranDwarf
6RukkDwarf
7ThrainDwarf
8MakinDwarf
9Roran IronhandDwarf
10Kaelin StoneforgerDwarf
11ThaurikDwarf
12Mikaelin StoneforgerDwarf
13RukhkarDwarf
14KaelthorinDwarf
15KorthinDwarf

qwen2.5:3b

IndexNameKind
1KorninDwarf
2KilgorinDwarf
3Grimmett-inDwarf
4KorninDwarf
5Gol-DurinDwarf
6Kor-darionDwarf
7Glim-Dun-inDwarf
8Grimmet-inDwarf
9Kilmarin-dorDwarf
10Grik-dorDwarf
11Kronar-dimDwarf
12Grik-DorDwarf
13Kilorin-dagorDwarf
14Glimm-knirDwarf
15Glim-DrivDwarf

nemotron-mini:4b

IndexNameKind
1IronheartDwarf
2Tombstone Forge-Fistdwarf
3Thundergrail-BristleDwarf
4Khorne-Thumbeddwarf
5Stonehammer Ironbearddwarf
6Ironhand GormDwarf
7Gron'karrin-GrunthorDwarf
8Grunthor IronclawDwarf
9Ironforgedwarf
10Grunthorn-SteelDwarf
11IronhammerDwarf
12Tinkering Thordwarf
13HardrockDwarf
14Grunthor StonefistDwarf
15Bolt-IronDwarf

Analysis: There is a significant improvement for smaller LLMs qwen2.5:0.5b and qwen2.5:1.5b. This confirms small models can be effective with proper guidance and appropriate data, offering better efficiency and energy consumption.

For more generation control, consider these options:

  • frequency_penalty: Reduces syllable/name style repetition

  • presence_penalty: Encourages result diversity

  • seed: Enables reproducible results when needed

Source code available at: https://github.com/ollama-tlms-golang/08-random-generation

I hope you enjoyed this article and see you soon.