Unidentified items: why the potion is bubbly this time

Rogue shuffled the names of its potions, scrolls, rings and wands every game, and forty years later the genre still does it. The code is thirty lines. Working out what the mechanic is for took me longer.

GRD 028 #512 Filed gamedev roguelike design

Contents
  1. What it's for
  2. What kills it
  3. The code

Pick up a potion in Rogue and the game calls it a blue potion. Next game the blue one is something else. Scrolls get titles glued together from random syllables, rings get stones, wands and staffs get a wood or a metal. Nothing tells you what any of it does until you use it, read a scroll of identify, or work it out.

Forty years on, NetHack, Brogue and DCSS all still do this, and new roguelikes drop it more often than they keep it. I kept it. Let's talk about why, and then about the code, because the code is the easy part.

What it's for

The obvious answer is mystery, and that's the weak one. Mystery wears off after the third game.

The better answer is that unidentified items turn a resource into a decision. A known potion of healing is a number in your inventory. An unknown potion is a question: do I drink this now, at full health in a cleared room, so that I know what it is when I'm at three hit points? Or do I save it and gamble when it matters? Every run, every player answers that differently, and the answer is a kind of play that no amount of dungeon generation gives you.

It also creates knowledge that belongs to the run. You learn that the bubbly one is poison this time, and that fact is worth something for the next twenty floors. Then the run ends and the knowledge dies with it. That's permadeath applied to information, and it's why the mechanic belongs to this genre and not to others.

What kills it

The mechanic dies the moment the gamble is never worth taking. If an unknown potion can be instant death, nobody drinks unknown potions, and you've built a system the player's correct response to is to ignore it. The bad outcomes have to be survivable, and survivable in a way that's interesting: blindness is a problem to solve, hallucination is funny, and a potion that halves your hit points forever is a reason to never drink again.

Brogue goes a step further and is honest about it. Its detect magic marks every item as benevolent or malevolent, so you know whether an unknown potion is good or bad before you drink it, just not which good or which bad. That halves the gamble and keeps the decision. NetHack lets you price items in a shop, because a shopkeeper's offer depends on what the thing really is, and a whole strategy grew around it. Both games let the player get smarter about the system rather than avoid it.

The code

Three pieces: a list of kinds, a list of appearances, and a shuffle that pairs them up at the start of the run. The shuffle has to come from the run's seed, which I wrote about in Deterministic randomness for a roguelike, so that a shared seed shares the potion colours too.

ts
// src/items/knowledge.ts
import { shuffle, stream } from '../rng';

export const POTIONS = ['healing', 'poison', 'haste', 'blindness', 'levitation', 'confusion', 'see invisible', 'restore strength'] as const;
export type Potion = (typeof POTIONS)[number];

const COLOURS = ['bubbly', 'murky', 'ruby', 'amber', 'smoky', 'milky', 'swirly', 'black'];

export class Knowledge {
  private appearance = new Map<Potion, string>();
  private known = new Set<Potion>();
  private called = new Map<Potion, string>();

  constructor(seed: number) {
    // One stream for this, so the colours depend on the seed and nothing else
    const colours = shuffle(stream(seed, 'potion-colours'), [...COLOURS]);
    POTIONS.forEach((kind, i) => this.appearance.set(kind, colours[i]));
  }

  identify(kind: Potion): void {
    this.known.add(kind);
  }

  call(kind: Potion, label: string): void {
    this.called.set(kind, label);
  }

  // What the inventory shows: the kind once known, the colour until then
  describe(kind: Potion): string {
    if (this.known.has(kind)) return `a potion of ${kind}`;
    const colour = this.appearance.get(kind);
    const label = this.called.get(kind);
    return label ? `a ${colour} potion called ${label}` : `a ${colour} potion`;
  }
}

describe is the only thing the inventory calls. Unknown is "a bubbly potion", known is "a potion of healing", and in between is "a bubbly potion called bad", because NetHack's naming command lets the player write their own label and I copied that. A player who drank it next to a grid bug and went blind wants to write "blind" on it, and will remember writing it.

Drinking calls identify, but only when the effect was visible. A potion of see invisible with nothing invisible in the room does nothing the player can notice, so it stays unknown, and that's its own small puzzle. NetHack does the same and it's one of the details that makes the system feel fair.

Two things to keep straight. The knowledge lives with the run and is saved with it, not with the item: identify one potion of healing and every potion of healing is known. And the appearance table is per run, so two potions of the same kind always look the same within a game.

Read more here: