Apeiron Manual

Apeiron is a MUD server written in Common Lisp. This manual is generated from the source code with 40ANTS-DOC: the narrative sections live next to the code that implements them, and the API reference is generated automatically from the exported symbols.

See the README for a quick start and the command reference.

Getting Started

Prerequisites

Start the Server

In SBCL:

(push #p"./" asdf:*central-registry*)
(ql:quickload :apeiron)
(apeiron.server:start-mud-server)

Or load run-mud.lisp:

sbcl --load run-mud.lisp

You should see:

[INFO] Initializing world...
[INFO] World initialized with 2 rooms
[INFO] MUD Server started on 127.0.0.1:8888

Connect as a Player

In another terminal:

telnet localhost 8888

Example Session

Note: the in-game eval command is restricted. Only characters owned by an admin account, or characters wearing an object with both the hat and wizard keywords (a "wizard hat"), may use it.

Using eval to create a room and connect it:

What is your name?
> Frodo

=== The Prancing Pony ===

You see:
  - Frodo (ID: 4)

Exits: west

Welcome to the MUD!
> eval (create-object! (world) (new-room :name "Rivendell"))
#<MUD-ROOM Rivendell (ID: 8)>
> eval (connect-rooms! (world) (here) "east" (obj-find "Rivendell") "west")
#<MUD-CONNECTION passage between The Prancing Pony and Rivendell (ID: 9)>
> look

=== The Prancing Pony ===

You see:
  - Frodo (ID: 4)

Exits: west, east

> go east
You go east.

=== Rivendell ===

You see:
  - Frodo (ID: 4)

Exits: west

> say Where are all the elves?
You say: Where are all the elves?

Stop the Server

In the SBCL REPL:

(apeiron.server:stop-mud-server)

This: 1. Sets *server-running* to NIL 2. Closes the server socket 3. Waits for acceptance thread to exit 4. Disconnects all characters

Architecture

       apeiron/core
     /     |        \
worlds  persistence  telnet
     \     |         /
         server
           |
       apeiron (meta)

Core is the shared foundation. Worlds and persistence build on it independently (no dependency between them). Telnet is standalone. The server layer wires everything together.

Key Design Principles

  1. Persistent Objects — game objects are persisted and changes are logged to enable recovery (using BKNR.Datastore).

  2. All power to the user — you can eval Lisp code directly within the game (could/should be restricted to admins in the future).

  3. Hot Reloading — no need to ever shut the server down for maintenance (WIP).

Protocols

Features

Currently Implemented

Planned

Development Guide

Adding a New Command

Commands are defined in src/command-handler.lisp using the DEFINE-COMMAND macro:

(define-command "wave" (world character args)
  (declare (ignore world args))
  (character-send-message character "You wave your hand."))

The macro takes: - Name: command string (will be lowercased) - Parameters: world (the mud-world instance), character (the character object), and args (raw argument string) - Body: command implementation

Example: More Complex Command

(define-command "examine" (world character args)
  (declare (ignore world))
  (let ((obj-name (string-trim '(#\Space #\Tab) args)))
    (if (zerop (length obj-name))
        (character-send-message character "Examine what?")
        (character-send-message character (format nil "You examine the ~A." obj-name)))))

Creating New Object Types

Extend the mud-object class:

(defclass mud-weapon (mud-object)
  ((damage :initarg :damage
           :accessor weapon-damage
           :initform 5)
   (weight :initarg :weight
           :accessor weapon-weight
           :initform 2)))

(defun create-weapon (&key (name "sword") (damage 5) (weight 2))
  (make-instance 'mud-weapon
                 :name name
                 :damage damage
                 :weight weight))

Using Object Properties

Objects have a flexible property storage system:

;; Set properties
(object-set-property character "experience" 1000)
(object-set-property room "dark" t)

;; Get properties
(object-get-property character "experience")  ; → 1000
(object-get-property room "dark")          ; → T

Building World Content

;; Create rooms
(defun build-world (world)
  (let ((tavern (new-room :name "The Tavern"
                          :description "A cozy tavern filled with travelers."))
        (forest (new-room :name "A Dense Forest"
                          :description "A dense forest with tall trees.")))

    ;; Register rooms in the world
    (world-add-object! world tavern)
    (world-add-object! world forest)

    ;; Connect rooms
    (connect-rooms! world tavern "north" forest "south")))

Broadcasting Messages

;; Message to all characters
(world-broadcast "A loud bell rings!")

;; Message to all except one
(world-broadcast "A wizard teleports away!" except-character)

Testing Commands

(ql:quickload :apeiron-test)
(apeiron-test:run-tests)

Or load run-tests.lisp:

sbcl --non-interactive --load run-tests.lisp

World

The world is the root object of an Apeiron game. It owns every room, area, character and item, assigns world-level IDs, and is the object that the persistence layer stores (see apeiron/persistence).

Create a world with new-world, add content with WORLD-ADD-OBJECT! and connect rooms with CONNECT-ROOMS!. See also Commands for the player-facing command layer. The full API of this package is generated automatically in the manual's API reference.

generic-function

Assign a world-level ID to an object, register it in the world's indices, and return the object.

generic-function
world room-a room-b &key to from name blocked blocked-message one-way one-way-message

Create a bidirectional Connection between ROOM-A and ROOM-B in WORLD.

TO is the direction name from ROOM-A to ROOM-B (e.g. "north"). FROM is the direction name from ROOM-B to ROOM-A (e.g. "south"). Each of TO and FROM accepts either a string (just the direction) or a list of strings whose first element is the direction and the remaining elements are synonyms (e.g. '("north" "n")). Neither may be NIL.

When BLOCKED is true the passage starts blocked and cannot be traversed. BLOCKED-MESSAGE is shown to characters when they try to pass.

ONE-WAY restricts the passage to a single direction: :BOTH (default) passable in either direction :A-TO-B passable only from ROOM-A to ROOM-B :B-TO-A passable only from ROOM-B to ROOM-A ONE-WAY-MESSAGE is shown when a character tries to traverse the passage the wrong way (e.g. "The slope is too steep to climb back up.").

The connection is linked into both rooms' CONNECTIONS lists and registered in the world.

Returns the registered mud-connection instance.

Place CHARACTER in the world's starting room and return the character. The character must already be registered in the world (via CREATE-OBJECT! or WORLD-ADD-OBJECT!). This only sets location and room membership — it does not assign IDs or index into world tables.

function
world message &optional exclude-character

Broadcast a message to all characters (optionally excluding one).

Register OBJECT in WORLD, materializing it for persistent worlds. For transient worlds this is equivalent to WORLD-ADD-OBJECT!. For persistent worlds a persistent copy is created in the datastore. When ROOM is provided, OBJECT is also placed in ROOM: its location is set to ROOM and it is added to ROOM's contents.

Create a copy of OBJECT, register it in WORLD, and return the copy.

Commands

The command system turns raw player input into handler calls. Every command is registered in *commands* by define-command and processed by handle-command — the full command pipeline, run through the world's parser (world-parser (1 2 3)). process-command is a convenience wrapper that routes input through the world's parser.

While a command runs, the eval helpers me, here and world are bound to the current character, room and world, which also makes them handy inside the in-game eval command.

variable
#<hash-table :test equal :count 27 {1204980903}>

Hash table of command handlers

macro
name (world character args) &body body

Define a command handler. world is the mud-world instance, CHARACTER is the character, ARGS is a raw string that the handler can parse as needed.

Process INPUT — a raw command line typed by PLAYER in WORLD — into a command and run it. This is the full command pipeline: the parser extracts the command name and arguments (resolving them against PLAYER and WORLD when it is context-aware), looks up the handler in *commands*, and runs it. Returns whatever the command handler returns (usually NIL).

PARSER is the parser object doing the work. The default MUD-PARSER method does a plain first-token split (see split-command) and executes the registered handler. Subclass mud-parser (or define your own methods on handle-command) to build derived parsers — for example one that understands prepositions and direct/indirect objects before dispatch.

Split INPUT into a command name and raw args string. Returns (values COMMAND-NAME RAW-ARGS-STRING). This is the raw building block the default parser uses; derived parsers can call it directly to reuse the plain first-token split.

function
world character command-string

Process a command from a character. Convenience wrapper around handle-command using the world's parser.

generic-function

Return the current player character during eval command execution.

Return the current character's location during eval command execution.

Return T if CHARACTER may use the in-game eval command: either their owning account is an administrator, or they are wearing an object with both the "hat" and "wizard" keywords.

Commands

The command system turns raw player input into handler calls. Every command is registered in *commands* by define-command and processed by handle-command — the full command pipeline, run through the world's parser (world-parser (1 2 3)). process-command is a convenience wrapper that routes input through the world's parser.

While a command runs, the eval helpers me, here and world are bound to the current character, room and world, which also makes them handy inside the in-game eval command.

variable
#<hash-table :test equal :count 27 {1204980903}>

Hash table of command handlers

macro
name (world character args) &body body

Define a command handler. world is the mud-world instance, CHARACTER is the character, ARGS is a raw string that the handler can parse as needed.

Process INPUT — a raw command line typed by PLAYER in WORLD — into a command and run it. This is the full command pipeline: the parser extracts the command name and arguments (resolving them against PLAYER and WORLD when it is context-aware), looks up the handler in *commands*, and runs it. Returns whatever the command handler returns (usually NIL).

PARSER is the parser object doing the work. The default MUD-PARSER method does a plain first-token split (see split-command) and executes the registered handler. Subclass mud-parser (or define your own methods on handle-command) to build derived parsers — for example one that understands prepositions and direct/indirect objects before dispatch.

Split INPUT into a command name and raw args string. Returns (values COMMAND-NAME RAW-ARGS-STRING). This is the raw building block the default parser uses; derived parsers can call it directly to reuse the plain first-token split.

function
world character command-string

Process a command from a character. Convenience wrapper around handle-command using the world's parser.

generic-function

Return the current player character during eval command execution.

Return the current character's location during eval command execution.

Return T if CHARACTER may use the in-game eval command: either their owning account is an administrator, or they are wearing an object with both the "hat" and "wizard" keywords.

Tutorial: Create a Secret Room

# World-Building Tutorial: Create a Secret Room

This tutorial walks you through creating a **password-protected secret room** containing a **secret diary** (guestbook).  
We'll use the in-game `eval` command, which runs Lisp code inside the live server — no restart required.

## The Plan

1. Create a hidden room: *The Ancient Library*
2. Create a secret diary (guestbook) and place it in the library
3. Connect the library to **the room you are standing in** through a *crack in the wall*
4. Lock the passage with a password challenge — characters must `answer` correctly to enter

## Step-by-Step

### 1. Connect to the server

```bash
telnet localhost 8888
```

Log in with any name. You'll start in the hub room, your current location.

### 2. Create the secret room

Use `eval` to create a new room:

```
> eval (world-add-object! (world) (new-room :name "The Ancient Library" :description "A dusty hidden library lit by a single flickering candle. Shelves of crumbling books line the walls."))

#<MUD-ROOM The Ancient Library (ID: 12)>
```

The return value shows the new room and its world-level ID (yours will differ — note it for later).

### 3. Create a secret diary (guestbook)

```
> eval (world-add-object! (world) (new-guestbook :name "a worn leather diary" :aliases '("diary")))

#<MUD-GUESTBOOK a worn leather diary (ID: 13)>
```

### 4. Place the diary in the secret room

```
> eval (container-add-object (world-object-by-id (world) 12) (world-object-by-id (world) 13))

#<MUD-GUESTBOOK a worn leather diary (ID: 13)>
```

Replace `12` and `13` with the IDs you got in steps 2 and 3.

### 5. Connect the library to your current room

```
> eval (connect-rooms! (world) (here) "north" (world-object-by-id (world) 12) "south" :name "a crack in the wall")

#<MUD-CONNECTION a crack in the wall (ID: 14)>
```

A character in the hub room can now `go north` and find the crack, and a character in the library can `go south` back.

### 6. Add the password challenge

Lock the connection with a password. Characters must type `answer <password>` to pass:

```
> eval (connection-set-challenge (connection-find (here) "north") "The wall whispers: 'Speak the password.'" "open-sesame" "has-heard-secret")

NIL
```

This sets up three things on the *crack in the wall* connection:

| Property | Your value | Purpose |
|---|---|---|
| `challenge-question` | `"The wall whispers: 'Speak the password.'"` | Shown to characters who try to pass without answering |
| `challenge-answer` | `"open-sesame"` | The correct answer (case-insensitive) |
| `challenge-flag` | `"has-heard-secret"` | A flag set on the character after a correct answer; once set, the character can pass freely |

### 7. Test it

Try going north:

```
> go north

The wall whispers: 'Speak the password.'
```

Give the wrong answer:

```
> answer swordfish

Wrong answer. Try again.
```

Give the correct answer:

```
> answer open-sesame

Correct! The way forward opens.
```

Go north again — now you enter the library:

```
> go north
You go north.

=== The Ancient Library ===

A dusty hidden library lit by a single flickering candle. Shelves of crumbling books line the walls.

You see:
  - a worn leather diary (ID: 13)
  - Frodo (ID: 4)

Exits: south
```

### 8. Read and write in the secret diary

```
> read diary

=== a worn leather diary ===

The diary is currently empty.

> write diary
What message do you want to write?
> Found the secret library at last!
You write your message in the diary.

> read diary

=== a worn leather diary ===

[2026-07-14 15:42:01] Frodo wrote:
  Found the secret library at last!
```

## Summary of Functions Used

| Function | Purpose |
|---|---|
| `new-room` | Create a new room |
| `new-guestbook` | Create a new guestbook (entries are persisted as CSV) |
| `world-add-object!` | Register an object/room in the world |
| `world-object-by-id` | Look up an object by its world-level ID |
| `container-add-object` | Place an object inside a container (room, character, etc.) |
| `connect-rooms!` | Create a bidirectional connection between two rooms |
| `connection-find` | Find the connection leaving a room in a given direction |
| `connection-set-challenge` | Lock a connection with a question/answer/flag challenge |
| `here` | Returns the current character's room (handy in `eval`) |
| `world` | Returns the current world (handy in `eval`) |

## Tips

- **Use `here` instead of IDs**: `(here)` returns the room you're standing in, so `(connect-rooms! (world) (here) "north" ...)` saves you from looking up IDs.
- **Check your work**: `(room-exit-list (here))` lists all exits from the current room.
- **All eval is persistent**: Every room, guestbook, and connection created this way is automatically saved to the BKNR datastore and survives server restarts.
- **Guestbook CSV persistence**: Guestbook entries are written to a CSV file in the `data/` directory and reloaded on server restart.

Tutorial: Wordle Puzzle Game

# Wordle Puzzle Game

A Wordle-like puzzle game you can drop into any room. Each puzzle has a secret 5-letter word, and characters guess it by telling the puzzle their guesses. Each character's progress is tracked independently, so everyone can play simultaneously.

## Create a Wordle Puzzle

Use `eval` to create a puzzle and place it in your current room:

```
> eval (let ((p (new-wordle-puzzle)))
         (container-add-object (here) p)
         (create-object! (world) p))

#<MUD-WORDLE-PUZZLE a Wordle puzzle board (ID: 25)>
```

The puzzle uses today's **daily word** — determined by the current date, so all
characters see the same word each day and it changes daily.  Each puzzle
instance picks its own random **seed**, so two puzzles created on the same day
will have *different* daily words — great for multiple boards in different
rooms.

| Parameter | Default | Description |
|---|---|---|
| `:name` | `"a Wordle puzzle board"` | Display name of the puzzle |
| `:description` | *(default description)* | What characters see when examining or viewing the board |
| `:target-word` | today's daily word | The 5-letter word to guess (omit for date-based daily word) |
| `:max-guesses` | `6` | How many guesses characters get |
| `:word-list` | `*wordle-default-words*` | A vector of valid 5-letter words to pick from |
| `:seed` | random | Per-instance seed for daily word selection; pass the same seed to get reproducible daily words |

**Portuguese (pt-BR) puzzles** — use the built-in Portuguese word list:

```
> eval (let ((p (new-wordle-puzzle
                :word-list *wordle-pt-br-words*)))
         (container-add-object (here) p)
         (create-object! (world) p))
```

Portuguese words include accented characters (á, â, ã, ç, é, ê, í, ó, ô, õ, ú).
Players can type plain ASCII letters and the puzzle matches them
accent-insensitively - e.g. guessing `macas` matches target `maçãs`.

**Reproducible puzzles** — pass the same `:seed` to create multiple instances
that share the same daily word rotation:

```
> eval (new-wordle-puzzle :seed 42 :name "Daily Challenge")
```

## Play the Game

Interact with the puzzle using the `tell` command (whisper privately to it — other characters won't see your guesses):

| Command | What it does |
|---|---|
| `tell <puzzle> <word>` | Make a guess (e.g. `tell board crane`) |
| `tell <puzzle> help` | Show instructions and colour guide |
| `tell <puzzle> show` | Show the current puzzle state |

The puzzle responds with a colour-coded board:
- **Green** letters are correct and in the right position
- **Yellow** letters are in the word but in the wrong position
- **Dim** letters are not in the word at all

Example session:

```
> tell board train
The board glows with coloured pegs...

  t r a i n
  · · · · ·
  · · · · ·
  · · · · ·
  · · · · ·
  · · · · ·

Speak a 5-letter word aloud (5 guesses remaining)

> tell board crane

  t r a i n
  c r a n e
  · · · · ·
  · · · · ·
  · · · · ·
  · · · · ·

You solved it in 2 guesses! The word was: CRANE
```

When someone solves or fails the puzzle, other characters in the room are notified:
```
Alice solved the Wordle puzzle!
```

## Advanced: Custom Puzzle

Create a puzzle with a specific word or custom settings:

```
> eval (let ((p (new-wordle-puzzle
                :name "Riddle Sphinx"
                :description "A wise stone sphinx awaits your guess."
                :target-word "quest"
                :max-guesses 4)))
         (container-add-object (here) p)
         (create-object! (world) p))

#<MUD-WORDLE-PUZZLE Riddle Sphinx (ID: 26)>
```

| Parameter | Default | Description |
|---|---|---|
| `:name` | `"a Wordle puzzle board"` | Display name of the puzzle |
| `:description` | *(default description)* | What characters see when examining or viewing the board |
| `:target-word` | today's daily word | The 5-letter word to guess (omit for date-based daily word) |
| `:max-guesses` | `6` | How many guesses characters get |
| `:word-list` | `*wordle-default-words*` | A vector of valid 5-letter words to pick from; see `*wordle-pt-br-words*` for Portuguese |
| `:seed` | random | Per-instance seed for daily word selection |

## Reset a Puzzle

Reset all character progress (keeping the same word) or set a new word:

```
> eval (wordle-reset (world-object-with-name (world) "Riddle Sphinx"))

T

> eval (wordle-reset (world-object-with-name (world) "Riddle Sphinx") :new-word "magic")

T
```

## Admin: Place in a Specific Room

Find a room by name and place the puzzle there:

```
> eval (let ((p (new-wordle-puzzle :target-word "world"))
             (room (world-object-with-name (world) "The Gathering")))
         (container-add-object room p)
         (create-object! (world) p))

#<MUD-WORDLE-PUZZLE a Wordle puzzle board (ID: 27)>
```

## Persistence

Wordle puzzles are fully persistent. The word list and max guesses are saved to the BKNR datastore. Per-character guess state and the daily word are ephemeral (the puzzle recalculates its daily word on server restart). If you want a permanent fixed word, pass `:target-word` explicitly when creating the puzzle. To add a puzzle to the default world permanently, include it in the world builder function in `src/worlds/apeiron.lisp`.

Persistence

The game world and every object in it live in a BKNR datastore. Transient game objects are converted to persistent counterparts (BKNR store-objects) whose changes are journaled, so everything survives restarts and crashes.

Declarative persistent classes

Persistent classes are declared as data in src/persistence/registry.lisp: a serapeum dict, *PERSISTENT-CLASS-REGISTRY*, maps each transient game class to an options dict.

(defparameter *persistent-class-registry*
  (dict
   'mud-object        (dict)
   'mud-room          (dict :transient-slots '(contents))
   'mud-character     (dict :transient-slots '(session))
   'mud-guestbook     (dict :transient-slots '(entries))
   'mud-world         (dict :transient-slots '(characters objects rooms areas))))

DEFINE-PERSISTENT-CLASSES reads the registry and defines the wrapping PERSISTENT-* classes: - name — PERSISTENT-<name> by default (MUD-ROOM → PERSISTENT-ROOM), overridable with :persistent-name; - superclasses — the transient class plus PERSISTENT-OBJECT when it is a MUD-OBJECT subtype, so every game object shares the same persistence behavior; overridable with :superclasses; - transient slots — :transient-slots lists the slots inherited from the transient class that must NOT be stored, for example a character's live session or a world's in-memory indices.

Adding a new persistent class is a one-line registry entry — no class definitions to hand-write.

Materialization and restore

MATERIALIZE-OBJECT converts a transient object into its persistent counterpart in place (preserving identity and cross-references). WORLD-RESTORE-OR-INITIALIZE restores the stored world on startup — or materializes a fresh one when no store exists yet.

Updating a running server

SAFE-UPDATE reloads changed code into a live image without a restart. It snapshots the datastore first (a rollback baseline), reloads the changed systems via RELOAD-APEIRON, and takes a second snapshot only when the persistent class schemas actually changed — the situation BKNR warns about, where the new schema must be persisted. If no class changed, the redundant second snapshot is skipped. See Deployment.

Data Migrations

BKNR persists objects against the class layout that existed when the snapshot / transaction was written. When code changes a persistent slot (rename, retype, remove), old datastores must be brought up to the current schema. BKNR itself offers only limited hooks for this (*slot-name-map*, CONVERT-SLOT-VALUE-WHILE-RESTORING), and they run at decode time / interactively, which is not usable for automatic migration.

This package therefore provides a small versioned migration framework:

Runner invocation points

world-restore-or-initialize calls run-data-migrations on an existing world before guest cleanup and index population, so migrated data is authoritative for everything downstream. A fresh world is stamped with the latest version at creation.

safe-update calls run-data-migrations after a hot reload so migrations ship with new code and apply as soon as that code is loaded — but safe-update only invokes the generic runner, so future migrations never require editing safe-update itself.

Why this is safe even when the running server's SAFE-UPDATE is the old

version

Migration is triggered by opening the datastore with the new code (world-restore-or-initialize at startup / after a reload), which always executes the current code. We never depend on the old safe-update knowing about a specific migration.

Renaming slots and decode

BKNR decodes an old snapshot against the current classes before run-data-migrations runs. A slot name from an old layout that no longer exists on the class will stop restore with an interactive rename/convert/ignore prompt — not something automatic migration can fix. A slot rename must therefore keep the old name decodable: either the class keeps a slot with the same symbol name (which is exactly what happened when mud-character's OWNER became the generic mud-object OWNER), or a migration runs that depends on that same-name decode (v1 below).

variable
(#S(DATA-MIGRATION :VERSION 1 :NAME "rename-character-owner-to-account" :FN #<FUNCTION (LAMBDA (WORLD) :IN "/home/runner/work/apeiron-mud/apeiron-mud/src/persistence/migrations.lisp") {B801A66DDB}>) #S(DATA-MIGRATION :VERSION 2 :NAME "convert-properties-and-config-to-fset-maps" :FN #<FUNCTION (LAMBDA (WORLD) :IN "/home/runner/work/apeiron-mud/apeiron-mud/src/persistence/migrations.lisp") {B801A0E09B}>))

Registered data migrations, kept sorted by ascending VERSION.

Add FN (a function of one argument, the persistent world) to the migration registry under VERSION. VERSION must be a positive integer.

The registry stays sorted by ascending VERSION. Re-registering an already-present VERSION replaces the old migration — this file is reloaded on every hot reload, so idempotent registration is required.

macro
version name (world-var) &body body

Define a data migration with VERSION and NAME. BODY runs with WORLD-VAR bound to the persistent world object while the datastore is open; it may read and write BKNR store objects and the world config.

VERSION is a positive integer that must be greater than every previously registered migration's version. NAME is a human-readable string used in log output.

Adding a new migration later only requires another define-data-migration form — no changes to safe-update or the restore path.

Return the data migration version recorded in WORLD's config, tolerating the legacy hash-table config of a pre-FSET datastore. A datastore written before version markers existed has no :data-version key and reports 0.

Return the highest migration version registered, or 0 if none.

Apply every registered data migration with version greater than WORLD's current data version, in ascending order.

Runs each migration in its own transaction so a failing migration rolls back cleanly, then advances the recorded version marker and snapshots the datastore once at the end. Returns the list of migrations that ran (each as a DATA-MIGRATION), or NIL when nothing was pending.

Deployment

Configuration

Edit src/constants.lisp:

(defconstant +server-host+ "127.0.0.1")  ; Change host
(defconstant +server-port+ 8888)         ; Change port
(defconstant +max-command-length+ 1024)  ; Max input length

Server Monitoring

;; Check status
(apeiron.server:get-server-status)

;; Get running characters
(apeiron.core:characters (apeiron.persistence:get-persistent-world))

;; Get all rooms
(apeiron.core:world-all-rooms (apeiron.persistence:get-persistent-world))

Updating a Running Server

Pull the latest code and reload it into the running image without a restart:

(apeiron.persistence:safe-update)

SAFE-UPDATE snapshots the datastore first (a rollback baseline), reloads the changed APEIRON systems via reload-apeiron, then takes a second snapshot only when persistent class definitions changed — so a new class schema is persisted. If nothing changed, the second snapshot is skipped.

After a reload, SAFE-UPDATE also runs any pending data migrations registered in the persistence module (see run-data-migrations). Migrations upgrade datastores written by older code — e.g. when a persistent slot was renamed — and are data-driven: adding a new migration never requires editing safe-update itself.

Because a migration ships in the same code load that an already-running old safe-update cannot know about, migrations are also run automatically from world-restore-or-initialize whenever a datastore is opened by the current code. Restarting the server with the new code is therefore always sufficient to migrate an old datastore — no manual step is needed, and you never depend on an old safe-update knowing about a brand-new migration.

For quick development reloads from inside the game, use the eval command:

eval (reload-apeiron)

Note on hot reloads and runtime configuration: server settings such as *server-ssl-certificate*, *server-ssl-key*, *server-port*, and *server-tls-port* are declared with defvar (not defparameter) on purpose. A reload leaves an already-bound configuration variable untouched, so the running listeners keep their settings. If these were defparameter, every reload would reset them to their defaults — for example wiping the TLS certificate from a live server, after which each new TLS connection fails the handshake with OpenSSL's opaque no shared cipher error.

Stopping the Server

(apeiron.server:stop-mud-server)

Troubleshooting

"Cannot find system :apeiron"

Make sure apeiron.asd is in the current directory and you've added it to ASDF:

(push #p"./" asdf:*central-registry*)

"Address already in use" (Port 8888)

Either: 1. Wait a minute for the port to be released 2. Change the port in src/constants.lisp 3. Kill the old process: pkill -f sbcl

Cannot connect with telnet

Verify: 1. Server is running (check SBCL output) 2. Port is correct (default 8888) 3. No firewall blocking connections 4. Try: telnet 127.0.0.1 8888

Dependency installation fails

Manually install dependencies:

(ql:quickload (list "usocket" "bordeaux-threads" "fiveam"))

Dependencies

All installed via Quicklisp automatically.

MCP Server (LLM Integration)

An MCP (Model Context Protocol) server is included in mcp/. It lets an LLM (Claude, Continue, etc.) connect to the MUD as a player character, issue commands, and run Lisp code in the game world.

See mcp/README.md for setup and usage.

API Reference

Automatically generated from the exported symbols of the apeiron.core package (and its small apeiron.core.utils helper package, plus the apeiron.persistence persistence layer).

APEIRON.CORE

Classes

CHARACTER-INPUT-EVENT

Issued whenever a player sends a line of input to the server.

Readers

The raw input line sent by the player.

CHARACTER-OUTPUT-EVENT

Issued whenever the server sends output to a player's session.

Readers

The output sent to the player's session.

LIMB

A body part that can hold or wear a single item. The item is stored in the limb's container-contents; the keywords it accepts are container-keywords (e.g. "hat" for a head, "weapon" for a hand). A hand limb also carries the "hand" keyword so hand-limb-p can tell holding limbs from wearing limbs.

MUD-ACCOUNT

A player account for the MUD.

Accounts are independent of the BKNR persistence layer and are stored in a plain .dat file under *data-directory*.

Characters are NOT linked from the account — only the character carries an OWNER reference (a string matching the account name).

Readers

When true, this account is an administrator. Admin-owned characters may use the in-game eval command.

Email address for password recovery.

Account login name (unique).

Salted PBKDF2 hash of the password, stored as "salt:hash" hex strings.

Accessors

When true, this account is an administrator. Admin-owned characters may use the in-game eval command.

Email address for password recovery.

Account login name (unique).

Salted PBKDF2 hash of the password, stored as "salt:hash" hex strings.

MUD-AREA

A named area: a set of rooms and the connections between them, backed by a CL-GRAPH graph so complex operations such as path finding and reachability queries can run on top of the plain room/connection data.

Readers

Flat list of mud-connection objects in this area.

Optional entrance room of the area (a mud-room), or NIL when the area has no designated entrance.

reader
(:graph = (make-instance 'cl-graph:graph-container :default-edge-type :undirected))

CL-GRAPH undirected graph whose vertices are mud-rooms and whose edges carry the mud-connection as their element.

Rooms in the area, as a list. A list (rather than a hash keyed by object-id (1 2)) so that rooms which have not yet been registered with a world — and therefore share the unset object-id (1 2) -1 — can still coexist in one area. The graph is the authoritative room set.

The mud-world this area is registered with, or NIL while the area is still being built transiently. Set by WORLD-ADD-AREA!. AREA-ADD-ROOM! and AREA-REGISTER-CONNECTION! use it to register incrementally added rooms/connections with the world (materializing them into the datastore for persistent worlds), mirroring WORLD-CONNECT-ROOMS!.

Accessors

Flat list of mud-connection objects in this area.

Optional entrance room of the area (a mud-room), or NIL when the area has no designated entrance.

accessor
(:graph = (make-instance 'cl-graph:graph-container :default-edge-type :undirected))

CL-GRAPH undirected graph whose vertices are mud-rooms and whose edges carry the mud-connection as their element.

Rooms in the area, as a list. A list (rather than a hash keyed by object-id (1 2)) so that rooms which have not yet been registered with a world — and therefore share the unset object-id (1 2) -1 — can still coexist in one area. The graph is the authoritative room set.

The mud-world this area is registered with, or NIL while the area is still being built transiently. Set by WORLD-ADD-AREA!. AREA-ADD-ROOM! and AREA-REGISTER-CONNECTION! use it to register incrementally added rooms/connections with the world (materializing them into the datastore for persistent worlds), mirroring WORLD-CONNECT-ROOMS!.

MUD-CHARACTER

A character in the MUD

Readers

The name (string) of the mud-account that owns this character. NIL for guest characters. Stored as a plain string so it survives BKNR restarts without needing an object reference.

reader
(:limbs = (default-character-limbs))

The character limbs (hands, head(s) etc.), a list of limb objects (see EQUIPMENT). Each limb's container-contents holds what is currently worn/held there. On persistent characters the limbs are materialized into the datastore so their contents persist.

The session controlling this character

Accessors

The name (string) of the mud-account that owns this character. NIL for guest characters. Stored as a plain string so it survives BKNR restarts without needing an object reference.

accessor
(:limbs = (default-character-limbs))

The character limbs (hands, head(s) etc.), a list of limb objects (see EQUIPMENT). Each limb's container-contents holds what is currently worn/held there. On persistent characters the limbs are materialized into the datastore so their contents persist.

The session controlling this character

MUD-CONNECTION

A connection between two rooms, with a direction spec at each end (a string, or a list of strings with synonyms). By default the connection is bidirectional; with :ONE-WAY set to :A-TO-B or :B-TO-A it can only be traversed in that direction. Characters cannot traverse a blocked connection.

Readers

Custom message shown when blocked (e.g. a riddle)

Whether the passage is currently blocked

Direction spec from ROOM-A to ROOM-B: a string (e.g. "north") or a list of strings whose first element is the direction and the rest are synonyms (e.g. '("north" "n")).

Direction spec from ROOM-B to ROOM-A: a string (e.g. "south") or a list of strings whose first element is the direction and the rest are synonyms (e.g. '("south" "s")).

Directionality of the passage: :BOTH (default — passable either way), :A-TO-B (only from ROOM-A to ROOM-B), or :B-TO-A (only from ROOM-B to ROOM-A).

Custom message shown when a character tries to traverse the passage the wrong way (e.g. a slope that only goes down).

First room in the connection

Second room in the connection

Accessors

Custom message shown when blocked (e.g. a riddle)

Whether the passage is currently blocked

Direction spec from ROOM-A to ROOM-B: a string (e.g. "north") or a list of strings whose first element is the direction and the rest are synonyms (e.g. '("north" "n")).

Direction spec from ROOM-B to ROOM-A: a string (e.g. "south") or a list of strings whose first element is the direction and the rest are synonyms (e.g. '("south" "s")).

Directionality of the passage: :BOTH (default — passable either way), :A-TO-B (only from ROOM-A to ROOM-B), or :B-TO-A (only from ROOM-B to ROOM-A).

Custom message shown when a character tries to traverse the passage the wrong way (e.g. a slope that only goes down).

First room in the connection

Second room in the connection

MUD-EVENT

Base class for all Apeiron MUD events.

Events that represent something that happened in the MUD world should inherit from this class rather than deeds:event directly, so that handlers can select for all MUD events with a single event-type specifier.

MUD-GUESTBOOK

A guestbook in which characters can read and write messages.

Readers

A list of plists containing guestbook entries, with keys :author, :message, and :timestamp.

File where the guestbook entries will be stored

Accessors

A list of plists containing guestbook entries, with keys :author, :message, and :timestamp.

File where the guestbook entries will be stored

MUD-NPC

A non-character character that can be fought in the MUD

Readers

Maximum damage per attack

Minimum damage per attack

reader
(:DEFEAT-MESSAGE = "The foe collapses!")

Message shown when the NPC is defeated

Whether this NPC has been defeated

Current hit points

Maximum hit points

Character property key set when this NPC is defeated

Accessors

Maximum damage per attack

Minimum damage per attack

accessor
(:DEFEAT-MESSAGE = "The foe collapses!")

Message shown when the NPC is defeated

Whether this NPC has been defeated

Current hit points

Maximum hit points

Character property key set when this NPC is defeated

MUD-OBJECT

Base class for all MUD objects

Readers

List of alternative name strings for matching

reader
(:created-at = (get-universal-time))

Universal time when this object was created.

The player (character) that created this object, or NIL when it was created by the world/author. Filled in automatically by CREATE-OBJECT! when a player command (e.g. eval) creates the object.

Object description

Unique identifier for this object

List of keywords representing the object

Location/container of this object

reader
(:NAME = "unnamed object")

Display name of the object

Owner of this object (e.g. a player or account name), or NIL when the object is unowned. Reserved for future use.

reader
(:properties = (fset:empty-map))

Extensible property storage: an immutable FSET hash map. On persistent objects a property change rebinds this slot with a new map — a genuine slot write that BKNR records — so no in-place hash-table mutation needs to be "touched".

Accessors

List of alternative name strings for matching

accessor
(:created-at = (get-universal-time))

Universal time when this object was created.

The player (character) that created this object, or NIL when it was created by the world/author. Filled in automatically by CREATE-OBJECT! when a player command (e.g. eval) creates the object.

Object description

Unique identifier for this object

List of keywords representing the object

Location/container of this object

accessor
(:NAME = "unnamed object")

Display name of the object

Owner of this object (e.g. a player or account name), or NIL when the object is unowned. Reserved for future use.

accessor
(:properties = (fset:empty-map))

Extensible property storage: an immutable FSET hash map. On persistent objects a property change rebinds this slot with a new map — a genuine slot write that BKNR records — so no in-place hash-table mutation needs to be "touched".

MUD-PARSER

Base command parser. A parser runs the full command pipeline for a line of player input via the handle-command generic: it extracts the command name and arguments (resolving them against the player and world when the parser is context-aware), looks up the handler, and runs it. The default method does a plain first-token split and executes the registered handler. Subclass mud-parser (or define your own methods on handle-command) to build derived parsers — for example one that understands prepositions and direct/indirect objects.

MUD-ROOM

A location/room in the MUD. A room may belong to at most one area (see room-area (1 2)); its exits are the union of the area's connections incident to it and its own CONNECTIONS list (see room-exit-connections).

Readers

The mud-area this room belongs to, or NIL when the room is not part of any area.

List of Connection objects attached to this room that are NOT part of an area: cross-area passages and legacy connections. Connections inside an area live in the area itself and are found through room-area (1 2).

Accessors

The mud-area this room belongs to, or NIL when the room is not part of any area.

List of Connection objects attached to this room that are NOT part of an area: cross-area passages and legacy connections. Connections inside an area live in the area itself and are found through room-area (1 2).

MUD-SESSION

A network session in the MUD

Readers

Character controlled by this session

Unique identifier for this object

Whether ANSI color codes should be used in output

Accessors

Character controlled by this session

Unique identifier for this object

Whether ANSI color codes should be used in output

MUD-SIGN

A sign with a fixed message that characters can read.

Built on readable-mixin, so the HANDLE-READ method defined there displays the message to any character that reads the sign.

MUD-WORDLE-PUZZLE

A Wordle-like puzzle object for the MUD.

Characters interact with the puzzle by telling it words. Each character's guesses are tracked independently. When a new day arrives the puzzle automatically rotates to that day's word and resets all progress.

Each puzzle instance has its own SEED for daily word selection, so different instances (in different rooms, e.g.) produce different words on the same date.

Each puzzle instance also tracks its own leaderboard of registered (non-guest) accounts — number of plays and correct solves.

Readers

reader
(:character-guesses = (make-hash-table :test #'equal))

Hash table mapping character-name => plist of guess data. Each entry is a plist with: :guesses - list of guess words (strings) :solved - whether this character solved it :failed - whether this character ran out of guesses

List of (account-name plays correct) tracking registered account stats on this puzzle. Persisted via BKNR.

Maximum allowed guesses per character.

reader
(:seed = (random most-positive-fixnum))

Per-instance random seed. Combined with the date to select the daily word, so different puzzle instances on the same date get different words.

The 5-letter word characters must guess.

reader
(:word-date = (wordle-date-key))

Integer date key (YYYYMMDD) of the current target-word.

reader
(:word-list = \*wordle-default-words\*)

Vector of valid words for random selection.

Accessors

accessor
(:character-guesses = (make-hash-table :test #'equal))

Hash table mapping character-name => plist of guess data. Each entry is a plist with: :guesses - list of guess words (strings) :solved - whether this character solved it :failed - whether this character ran out of guesses

List of (account-name plays correct) tracking registered account stats on this puzzle. Persisted via BKNR.

Maximum allowed guesses per character.

accessor
(:seed = (random most-positive-fixnum))

Per-instance random seed. Combined with the date to select the daily word, so different puzzle instances on the same date get different words.

The 5-letter word characters must guess.

accessor
(:word-date = (wordle-date-key))

Integer date key (YYYYMMDD) of the current target-word.

accessor
(:word-list = \*wordle-default-words\*)

Vector of valid words for random selection.

MUD-WORLD

Configuration root for the MUD world. Rooms, guestbooks, and other objects are stored as independent BKNR persistent objects.

Readers

reader
(:areas = (make-hash-table :test #'eql))

All areas in the world, keyed by world-level ID.

reader
(:characters = (make-hash-table :test #'equal))

Stores all online/active characters in world

reader
(:config = (fset:empty-map))

Configuration map (keys are keywords). An outer, immutable FSET hash map so a config change rebinds the slot with a new value — an ordinary slot write that BKNR records.

Monotonic ID counter for assigning world-level IDs.

reader
(:objects = (make-hash-table :test #'eql))

All objects in the world, keyed by world-level ID.

reader
(:parser = (make-instance 'mud-parser))

Command parser used to turn player input into command name and args.

reader
(:rooms = (make-hash-table :test #'eql))

All rooms in the world, keyed by world-level ID.

Accessors

accessor
(:areas = (make-hash-table :test #'eql))

All areas in the world, keyed by world-level ID.

accessor
(:characters = (make-hash-table :test #'equal))

Stores all online/active characters in world

accessor
(:config = (fset:empty-map))

Configuration map (keys are keywords). An outer, immutable FSET hash map so a config change rebinds the slot with a new value — an ordinary slot write that BKNR records.

Monotonic ID counter for assigning world-level IDs.

accessor
(:objects = (make-hash-table :test #'eql))

All objects in the world, keyed by world-level ID.

accessor
(:parser = (make-instance 'mud-parser))

Command parser used to turn player input into command name and args.

accessor
(:rooms = (make-hash-table :test #'eql))

All rooms in the world, keyed by world-level ID.

READABLE-MIXIN

Objects that contain a message which can be read should (or could?) use this mixin

Readers

String that will be shown when character reads object

Accessors

String that will be shown when character reads object

SESSION-EVENT

Base class for events tied to a particular player session.

Readers

Name of the character (or NIL if not yet set).

Unique identifier of the session that triggered the event.

STREAM-SESSION

A session backed by a plain Common Lisp stream. Useful for testing with string streams, or for Telnet-like backends that provide their own stream abstraction.

Readers

The stream backing this session

Generics

Add new keyword, ignore if existing

Apply STAT's side effects when CHARACTER gains a level in it. STAT is a canonical keyword (see STAT-KEY). The default does nothing; specialize per stat — e.g. STAMINA raises maximum HP.

Return the yellow message shown when a character's STAT grows. Deliberately free of numbers — it conveys the feeling of progress.

generic-function
world room-a room-b &key to from name blocked blocked-message one-way one-way-message

Create a bidirectional Connection between ROOM-A and ROOM-B in WORLD.

TO is the direction name from ROOM-A to ROOM-B (e.g. "north"). FROM is the direction name from ROOM-B to ROOM-A (e.g. "south"). Each of TO and FROM accepts either a string (just the direction) or a list of strings whose first element is the direction and the remaining elements are synonyms (e.g. '("north" "n")). Neither may be NIL.

When BLOCKED is true the passage starts blocked and cannot be traversed. BLOCKED-MESSAGE is shown to characters when they try to pass.

ONE-WAY restricts the passage to a single direction: :BOTH (default) passable in either direction :A-TO-B passable only from ROOM-A to ROOM-B :B-TO-A passable only from ROOM-B to ROOM-A ONE-WAY-MESSAGE is shown when a character tries to traverse the passage the wrong way (e.g. "The slope is too steep to climb back up.").

The connection is linked into both rooms' CONNECTIONS lists and registered in the world.

Returns the registered mud-connection instance.

generic-function
generic-function

Create a copy of OBJECT, register it in WORLD, and return the copy.

Register OBJECT in WORLD, materializing it for persistent worlds. For transient worlds this is equivalent to WORLD-ADD-OBJECT!. For persistent worlds a persistent copy is created in the datastore. When ROOM is provided, OBJECT is also placed in ROOM: its location is set to ROOM and it is added to ROOM's contents.

Process INPUT — a raw command line typed by PLAYER in WORLD — into a command and run it. This is the full command pipeline: the parser extracts the command name and arguments (resolving them against PLAYER and WORLD when it is context-aware), looks up the handler in *commands*, and runs it. Returns whatever the command handler returns (usually NIL).

PARSER is the parser object doing the work. The default MUD-PARSER method does a plain first-token split (see split-command) and executes the registered handler. Subclass mud-parser (or define your own methods on handle-command) to build derived parsers — for example one that understands prepositions and direct/indirect objects before dispatch.

Called on OBJECT when an EVENT is issued.

Game objects (NPCs, rooms, items, etc.) can specialise this generic function to react to events they care about. The default implementation is a no-op.

To receive events, an object must also register itself as a handler on the event loop via DEEDS:REGISTER-HANDLER (or the higher-level helpers above).

Example:

(defmethod handle-event ((npc mud-npc) (event character-input-event)) (when (string-equal (input event) "hello") (format t "NPC ~A heard hello!~%" (object-name npc))))

;; Register the NPC: (deeds:with-handler character-input-event (ev) (handle-event my-npc ev))

Called when WRITER holds OBJECT in a hand. Should record the message and return non-NIL. Returns NIL if the object has nothing to say.

Called when READER tries to read OBJECT. Should display the readable content to the reader and return non-NIL. Returns NIL if the object has nothing readable.

Called when SPEAKER directs MESSAGE at OBJECT. Returns non-NIL if the speech was handled, NIL otherwise.

Called when WRITER wears OBJECT on their body (e.g. a hat on the head). Should record the message and return non-NIL. Returns NIL if the object has nothing to say.

Called when WRITER tries to write MESSAGE on OBJECT. Should record the message and return non-NIL. Returns NIL if the object is not writable.

Read a line of input with masked echo (password). The default method falls back to mud-read-line without masking. Subclasses should override to provide proper masking.

Returns a copy of object with id set to -1 and location to nil

Get a long description of an object for detailed examination: its name, description slot (when non-empty), and any type-specific detail such as worn items or hit points. Room listings use object-short-description instead.

Set a property value on an object. Returns VALUE.

Get a short description of an object — just enough to identify it in a listing (room contents, inventory, worn items). Short descriptions never include the object's description text, worn items, or other verbose detail. Specialized methods on subclasses provide appropriate coloring.

generic-function
object walker from-room to-room direction

Called as a consequence of WALKER moving from FROM-ROOM to TO-ROOM in DIRECTION, once for each item WALKER is wearing or holding. The item gets a chance to react to the movement (send messages, trigger effects, ...).

Returns a sound string (e.g. "click-clack") if the item makes a noticeable noise while moving, or NIL if it is silent. The default method returns OBJECT's "stepping-sound" property (if any), so plain items can be given a stepping sound via object-set-property; subclasses may override for computed behavior.

Remove keyword, ignore if not present

Return T while SESSION's transport is still connected.

The default method returns T: a session with no observable network transport is always considered alive. Network subclasses (e.g. TELNET-SESSION) override this to report a dropped or deliberately closed connection, so the game loop can stop polling a dead session instead of performing one more doomed — and possibly error-signalling — I/O operation on it.

Clean up and disconnect this session.

Send a keepalive heartbeat for this session. The default method is a no-op.

Send a prompt to the character on the same line (no newline). The default method sends "> " and returns. Subclasses (e.g. telnet-session) may override to add protocol-specific signalling (e.g. EOR for prompt detection).

Push SESSION's current view of CHARACTER's state to the client.

This is a protocol-agnostic hook: the core never decides how state is transmitted. The default method does nothing, so non-networked sessions (and sessions whose transport has no structured channel) are unaffected.

Network subclasses override it to emit whatever the client understands — for example a telnet session sends GMCP Char.Vitals / Char.Stats messages. It is called after login and after each processed command, so it must be cheap and idempotent.

Remove OBJECT from whichever limb holds it and return it to CHARACTER's inventory.

Returns (values item limb), where limb is the limb it was removed from, or (values nil nil) if OBJECT was not equipped.

generic-function
character object &optional limb

Equip OBJECT on CHARACTER. LIMB may be a LIMB, a limb name string (e.g. "head", "left hand"), or NIL to wear on the first limb whose keywords fit (reporting :occupied if that limb is already taken).

Returns (values limb reason): limb — the limb equipped (or the fitting limb for :occupied / :keywords-dont-match), or NIL on other failures reason — :ok, :no-such-limb, :no-fitting-limb, :keywords-dont-match, :occupied, or :not-in-inventory

Register AREA and everything in it (rooms, contained objects, and connections) in WORLD.

Each room and connection in the area is registered with the world (assigning world-level IDs and materializing them for persistent worlds), as is every non-character object contained in the area's rooms (NPCs, guestbooks, items). The area itself is then registered and indexed in world-areas (1 2). The call is idempotent: objects that are already registered are simply re-indexed.

A room may belong to at most one area: if any room in AREA already belongs to a different area of WORLD, an error is signaled before anything is registered.

The generic function is specialized on WORLD so persistent worlds can materialize the whole closure into the datastore in a single transaction before indexing it. Returns AREA.

Remove OBJECT from all world indices (world-objects, world-characters, world-rooms, world-areas). The default method handles hash-table removal; the persistence layer specialises this to also destroy the BKNR object. Returns the removed OBJECT.

Functions

Return T if an account with the given NAME (case-insensitive) exists.

function
direction-or-connection &rest args

Add synonyms to a direction spec or to an existing connection.

With a string as the first argument it builds a direction spec: (add-synonym "north") => "north" (add-synonym "north" "n") => ("north" "n")

With a mud-connection as the first argument it adds SYNONYMS to the end of the connection whose primary direction matches DIRECTION-STRING (case-insensitive), mutating the connection and returning it: (add-synonym conn "south" "s")

function
character direction from-room to-room

Announce CHARACTER moving from FROM-ROOM to TO-ROOM in DIRECTION to the other characters present.

Characters left behind in FROM-ROOM are told " went "; characters already in TO-ROOM are told " arrives from " (the direction as seen from TO-ROOM, i.e. the exit the mover came through).

If CHARACTER is wearing or holding an item that reacts to movement (see on-movement), its sound prefixes the announcements: "You hear a click-clack sound as ...".

Returns (values WENT-DIRECTION SOUND): the canonical direction name (as seen from FROM-ROOM, useful for the mover's own message) and the sound made by CHARACTER's worn items (so the mover can hear it too).

Add ROOM to AREA (idempotent per object identity). Registers the room as a vertex of the area's graph and in the rooms list, and sets the room's room-area (1 2) back-reference to AREA. Returns ROOM.

When AREA is already registered with a world (area-world (1 2) non-NIL), ROOM is registered with that world via CREATE-OBJECT! first — the same path WORLD-CONNECT-ROOMS! uses — so for persistent worlds an incrementally added room is materialized into the datastore instead of being left as a transient mud-room that BKNR cannot encode. CREATE-OBJECT! is a harmless re-registration for rooms that are already registered.

Return the list of rooms directly connected to ROOM in AREA.

function
area north-room south-room &rest args

Connect NORTH-ROOM (left arg) south to SOUTH-ROOM (right arg) inside AREA. From SOUTH-ROOM you go north to NORTH-ROOM. Standard cardinal synonyms are added: "s" from north-room, "n" from south-room.

function
area room-a room-b &key to from name blocked blocked-message one-way one-way-message

Create a bidirectional Connection between ROOM-A and ROOM-B inside AREA.

TO is the direction from ROOM-A to ROOM-B (e.g. "north"); FROM is the direction from ROOM-B to ROOM-A (e.g. "south"). Each accepts a string or a list of strings with synonyms (e.g. '("north" "n")). Neither may be NIL. BLOCKED / BLOCKED-MESSAGE behave like CONNECT-ROOMS!.

ONE-WAY restricts the passage to a single direction (:A-TO-B or :B-TO-A, default :BOTH); ONE-WAY-MESSAGE is the message shown when a character tries to traverse it the wrong way.

Creates the mud-connection, registers it in the area graph and returns it.

function
area west-room east-room &rest args

Connect WEST-ROOM (left arg) east to EAST-ROOM (right arg) inside AREA. From EAST-ROOM you go west to WEST-ROOM. Standard cardinal synonyms are added: "e" from west-room, "w" from east-room.

Return the number of connected components in AREA (0 when empty).

Implemented with the graph's transitive closure: repeatedly pick an unvisited room, remove its whole reachable set, and count.

Return non-NIL if every room in AREA is reachable from every other (and the area contains at least one room).

Return the number of connections in AREA.

Return every defeated NPC standing in AREA's rooms.

Find a room in AREA by world-level ID (an integer) or by name (a string, case-insensitive). Returns the room or NIL.

Integer lookups scan the rooms list; name lookups scan the graph-derived room list, which is authoritative and complete even when a restored snapshot's ROOMS slot was incomplete.

Return every NPC standing in AREA's rooms, in room order.

Only NPCs held directly by a room are reported; NPCs nested inside other containers (a chest, another character's inventory) are not.

Return non-NIL if ROOM-B can be reached from ROOM-A via connections in AREA.

Return every room reachable from ROOM via connections in AREA (including ROOM itself), respecting one-way connections: a one-way passage can only be used from its passable end.

Rebuild the area's cl-graph index from its ROOMS and CONNECTIONS slots.

The graph is a derived index; this resets it to a fresh graph-container and re-adds every room as a vertex and every connection as an edge. It is called automatically after a persistent restore (the graph is a transient slot and is not stored) and is useful after directly editing the room/connection lists. Each room's room-area (1 2) back-reference is also restored, so areas restored from older snapshots (whose rooms predate the back-reference) still find their connections. Returns AREA.

Register an existing mud-connection in AREA.

Both endpoint rooms are added to the area (as vertices, setting their room-area (1 2) back-reference), an undirected edge carrying CONNECTION is added to the graph, and the connection is pushed onto area-connections (1 2). The connection is deliberately NOT pushed onto the rooms' CONNECTIONS lists: area connections live in the area and are found through room-exit-connections. Idempotent per room pair: an area holds at most one connection between two rooms.

When AREA is registered with a world (area-world (1 2) non-NIL), CONNECTION is registered with that world via CREATE-OBJECT! before being stored — mirroring WORLD-CONNECT-ROOMS! — so for persistent worlds a connection created here (e.g. through AREA-CONNECT-ROOMS!) is materialized into the datastore instead of remaining a transient mud-connection.

Returns CONNECTION.

Remove CONNECTION from AREA: drop its graph edge and remove it from area-connections (1 2). The endpoints' room-connections (1 2) lists are also cleaned up as a legacy safeguard (older worlds stored area connections there). Returns CONNECTION.

Remove ROOM from AREA along with every connection incident to it. The room and its connections are removed from the graph, the rooms list and the connections list; the endpoints' room-connections (1 2) lists are also cleaned up, and the room's room-area (1 2) back-reference is cleared. Returns ROOM.

Bring every defeated NPC in AREA's rooms back to life at full hit points (see NPC-RESURRECT!). Returns the list of NPCs revived, or NIL when none were defeated.

Return the list of mud-connections incident to ROOM in AREA.

Return the number of rooms in AREA.

Return the list of all rooms in AREA (derived from the graph's vertices).

Return non-NIL if ROOM is part of AREA.

Return the list of mud-connections traversed by a shortest path from ROOM-A to ROOM-B, or NIL if ROOM-B is unreachable from ROOM-A.

Set ROOM as the entrance of AREA, or clear it with NIL.

ROOM should be one of the area's rooms (or NIL). Returns ROOM.

Return the shortest path from ROOM-A to ROOM-B as a list of rooms (inclusive), or NIL if ROOM-B is unreachable from ROOM-A.

The path is shortest in the number of hops (all edges count equally) and respects one-way connections: a one-way passage can only be used from its passable end. For ROOM-A = ROOM-B the trivial path (ROOM-A) is returned.

function
obj question &key (default "") secret (timeout 300)

Asks input from the user. When SECRET is non-NIL the input is not echoed (for passwords etc.). TIMEOUT is the maximum seconds to wait for input (default 300); on timeout the read returns the default with status :TIMEOUT so idle-but-connected callers can keep the connection alive and re-ask.

Returns (values ANSWER STATUS). ANSWER is the trimmed input, or DEFAULT when the user gave no usable input. STATUS is NIL on success, or the read status (:eof, :timeout, :connection-lost) when no input could be read — callers that need to detect a disconnected client should check STATUS.

Authenticate an account by NAME and PASSWORD. Returns the mud-account on success, or NIL on failure.

Wrap TEXT in ANSI blue foreground.

Wrap TEXT in ANSI bold.

Wrap TEXT in ANSI bold + cyan foreground.

Wrap TEXT in ANSI bold + green foreground.

Wrap TEXT in ANSI bold + red foreground.

Wrap TEXT in ANSI bold + bright white foreground.

Wrap TEXT in ANSI bold + yellow foreground.

Wrap TEXT in ANSI bright blue foreground.

Wrap TEXT in ANSI bright cyan foreground.

Wrap TEXT in ANSI bright green foreground.

Wrap TEXT in ANSI bright magenta foreground.

Wrap TEXT in ANSI bright red foreground.

Wrap TEXT in ANSI bright white foreground.

Wrap TEXT in ANSI bright yellow foreground.

Return the standard direction spec for a cardinal DIRECTION: the direction name plus its conventional single-letter synonym.

(cardinal-spec "north") => ("north" "n") (cardinal-spec "south") => ("south" "s") (cardinal-spec "east") => ("east" "e") (cardinal-spec "west") => ("west" "w")

Signals an error for non-cardinal directions. The result can be passed directly as the :TO / :FROM argument of CONNECT-ROOMS!.

Return T if CHARACTER's owning account is an administrator. Guest characters (no account) are never administrators.

CHARACTER attacks NPC, returning the list of messages to send to the character. Damage comes from CHARACTER-MELEE-ROLL (strength plus weapon); the strike message names the weapon in use — or says the character is striking bare-handed — and spells out the full damage calculation. Each attack also banks +character-strength-per-attack+ strength points, announcing the yellow level-up message when a level is reached.

Award POINTS toward CHARACTER's STAT and, when a level is gained, send the stat's yellow level-up message to the character. This is the shared entry point for every action that grows a stat. Returns the number of levels gained.

Get a character by ID.

Return (values MIN MAX) the melee damage range of CHARACTER: the held weapon's range (see weapon-damage-range), or the bare-handed range when no weapon is held.

Give CHARACTER a current-HP value if it does not have one yet. Maximum HP is derived from stamina (see character-max-hp), so nothing needs to be stored for it — the stat stays lazily initialized and saved characters need no migration.

Add POINTS toward CHARACTER's STAT, raising the level as thresholds are reached — possibly more than once — and applying each level-up's effects via CHARACTER-STAT-LEVEL-UP!. Returns the number of levels gained.

Return the first weapon CHARACTER is holding, or NIL if none. A weapon is any item carrying the "weapon" keyword (see weapon-p).

Return CHARACTER's current hit points, defaulting lazily to maximum.

Return CHARACTER's intelligence level. Intelligence grows by solving Wordle puzzles.

Points CHARACTER has banked toward the next intelligence level.

Return CHARACTER's maximum hit points, derived from stamina.

function
character from-room to-room direction

Return the sound (e.g. "click-clack") made by the first item CHARACTER is wearing or holding that reacts to moving from FROM-ROOM to TO-ROOM in DIRECTION, or NIL if no worn item makes a sound. See on-movement.

Roll CHARACTER's melee damage for one attack: the strength bonus plus a roll from the damage range of the weapon being held — or the bare-handed range when no weapon is held. See CHARACTER-MELEE-ROLL for the full breakdown.

function
character message &key (newline t)

Send a message to a character. If NEWLINE is nil, don't add a trailing newline. Honors the session's color preference by binding *colorize* around the write. If the character has no session (e.g. disconnected), the message is silently dropped.

Return CHARACTER's stamina level. Stamina grows by walking.

Points CHARACTER has banked toward the next stamina level.

Return CHARACTER's level in STAT, lazily storing the base value the first time it is read, so saved characters need no data migration.

Return the points CHARACTER has banked toward STAT's next level.

Return CHARACTER's strength level. Strength grows by attacking.

Return the melee damage bonus granted by CHARACTER's strength: one point per +character-strength-per-damage+ levels above the base, so a starting character adds nothing and the bonus grows with the stat.

Points CHARACTER has banked toward the next strength level.

Return T if CHARACTER is currently wearing or holding an item whose keywords include every keyword in KEYWORDS (case-insensitive).

Return an alist of (limb . ITEM) for every item currently worn or held, in limb order.

Get all active (online) characters — those with a live session.

Return the list of mud-character objects currently in ROOM.

mud-character is defined in character.lisp, which loads after this file, so the class is looked up at runtime via FIND-CLASS.

Verify PASSWORD against a stored PASSWORD-HASH (as produced by hash-password). Returns T if the password matches, NIL otherwise.

function
text &rest sgr-params

Wrap TEXT in an ANSI SGR escape sequence. SGR-PARAMS are integers (e.g. +SGR-FG-RED+, +sgr-bold+). Always appends reset at the end. Respects *colorize* — returns plain TEXT when *colorize* is NIL.

Examples: (color-text "Red Alert!" +sgr-fg-red+ +sgr-bold+) (color-text "Green text" +sgr-fg-green+)

function
&key (log-directory \*data-directory\*)

Initialise log4cl for the MUD.

LOG-DIRECTORY is the directory where log files are written; defaults to *data-directory*. Configuration is idempotent.

Behaviour is driven by *run-mode*: :PROD — file + console at :INFO :DEBUG — file + console at :DEBUG :TEST — file-only at :INFO

function
world north-room south-room &rest args

Connect NORTH-ROOM (left arg) south to SOUTH-ROOM (right arg). From SOUTH-ROOM you go north to NORTH-ROOM. Standard cardinal synonyms are added: "s" from north-room, "n" from south-room.

function
world west-room east-room &rest args

Connect WEST-ROOM (left arg) east to EAST-ROOM (right arg). From EAST-ROOM you go west to WEST-ROOM. Standard cardinal synonyms are added: "e" from west-room, "w" from east-room.

Return the primary direction name that leads out of ROOM through CONNECTION.

Return a blocking message if a connection in this direction is blocked, or nil.

When the connection has a custom BLOCKED-MESSAGE (e.g. a riddle question) that is returned; otherwise a generic "X is blocked" message is used.

Find a connection from ROOM in the given DIRECTION, or nil.

Searches the room's exit connections (area connections first, then the room's own, see room-exit-connections) for a connection that has this room and direction (including direction synonyms). Returns the connection if found.

Return non-NIL if CONNECTION is one-way (not passable in both directions).

Return the room at the other end of CONNECTION from ROOM.

function
connection question answer flag

Set a challenge (riddle/password) on a CONNECTION.

A character who answers correctly with ANSWER sets the FLAG on themselves, which allows them to pass. Characters without the flag see the QUESTION as a blocking message. This is independent of regular connection blocking (connection-blocked-p (1 2)).

Return non-NIL if CONNECTION can be traversed from ROOM.

Bidirectional connections (:BOTH) are always usable. One-way connections are usable only from their passable end: :A-TO-B from ROOM-A, :B-TO-A from ROOM-B.

Return non-NIL if CONTAINER currently holds no objects.

Return non-NIL if CONTAINER is at its MAX-CAPACITY. A container with NIL capacity is never full.

Wrap TEXT in ANSI cyan foreground.

Describe OBJECT, capturing output to a string. Like CL:DESCRIBE but returns a string instead of printing to standard-output. Hash-tables and FSET maps are shown entry by entry (see hashmap), since DESCRIBE does not print their contents. Example: (d (me))

Return the standard set of limbs for a humanoid character: a head, left and right hands, and feet. Hand limbs carry the "hand" keyword so hand-limb-p can distinguish holding from wearing.

Remove CHARACTER from their current room location. Sets location to NIL and removes from the room's contents. Does NOT touch world indices — use WORLD-REMOVE-OBJECT! for that.

Unequip everything CHARACTER is wearing and drop all carried items into their current room (if any), so the items survive the character's removal. Used when removing guest characters.

Return T if CHARACTER may use the in-game eval command: either their owning account is an administrator, or they are wearing an object with both the "hat" and "wizard" keywords.

Return the mud-account with the given NAME, or NIL if not found. Matching is case-insensitive.

Find a character in the world owned by ACCOUNT-NAME (a string), or NIL.

Find a character in a room by name.

Return the limb of CHARACTER whose name matches NAME (case-insensitive), or NIL.

Return the limb of CHARACTER currently holding OBJECT, or NIL.

Find a living NPC in a room by partial name match or alias.

Format an exit direction with its synonyms in parentheses.

Cardinal directions (north/south/east/west) with a matching single-letter synonym get the compact format: (n)orth, (s)outh, (e)ast, (w)est. Other directions with synonyms show: Direction (syn). Directions without synonyms are shown as-is.

Examples: north + '("n") => (n)orth south + '("s") => (s)outh Puzzling Forest + '("pf") => Puzzling Forest (pf) north + nil => north

Get a configuration value from the world config, or NIL when unset.

Wrap TEXT in ANSI green foreground.

Add a new message to the guestbook.

Format the guestbook entries as a readable string.

Read a CSV file and return a list of entry plists.

Return non-NIL if LIMB is a hand — i.e. an item is held in it rather than worn on it. Hands are limbs carrying the "hand" keyword.

Hash PASSWORD using PBKDF2 with a random 16-byte salt. Returns a string of the form "salt-hex:derived-key-hex" suitable for storage in mud-account.

Return the contents of a hash-table or FSET map as a string, one KEY => VALUE per line. Handy in eval because CL:DESCRIBE does not print a collection's entries. Example: (hashmap (object-properties (me)))

Return the current character's location during eval command execution.

Return the contents of CONTAINER as a string. Example: (inv (here))

function
session-id character-name input-line

Issue a character-input-event for the given session and input line.

function
session-id character-name output-text

Issue a character-output-event for the given session and output text.

function
format-string &rest format-args

Issue an error message as a Deeds error-event.

function
format-string &rest format-args

Issue an informational message as a Deeds info-event.

function
format-string &rest format-args

Issue a warning message as a Deeds warning-event.

Return non-NIL if OBJECT may be stored in CONTAINER.

A container with no keyword restrictions accepts anything; otherwise OBJECT's keywords must overlap CONTAINER's allowed keywords.

Return the single item currently held/worn in LIMB, or NIL.

Load accounts from the accounts data file into *accounts*. Existing in-memory accounts are preserved (not cleared) so that reloading does not lose data that hasn't been saved yet.

Return the location chain of OBJECT as a string, from innermost to outermost. Example: (loc (me)) — shows character -> room -> world containment.

Wrap TEXT in ANSI magenta foreground.

function
ROOM-A DIRECTION-A ROOM-B DIRECTION-B &KEY (NAME (FORMAT NIL "passage between ~A and ~A" (OBJECT-NAME ROOM-A) (OBJECT-NAME ROOM-B))) BLOCKED BLOCKED-MESSAGE ONE-WAY ONE-WAY-MESSAGE

Create and return a new mud-connection between ROOM-A and ROOM-B.

DIRECTION-A is the direction spec from ROOM-A to ROOM-B (e.g. "north"). DIRECTION-B is the direction spec from ROOM-B to ROOM-A (e.g. "south"). Each spec is a string (just the direction name) or a list of strings whose first element is the direction and the remaining elements are synonyms (e.g. '("north" "n")). Use add-synonym to build specs. When BLOCKED is true the passage starts blocked. BLOCKED-MESSAGE is shown to characters when they try to pass.

ONE-WAY controls the passage's directionality: :BOTH (default) passable in either direction :A-TO-B passable only from ROOM-A to ROOM-B :B-TO-A passable only from ROOM-B to ROOM-A ONE-WAY-MESSAGE is shown when a character tries to traverse the passage the wrong way (e.g. "The slope is too steep to climb back up.").

The connection is NOT linked into any room's connections list or world; call CONNECT-ROOMS (in world.lisp) for that.

function
&KEY (NAME "limb") (KEYWORDS NIL)

Create a limb. KEYWORDS are the item keywords allowed in it.

Return the current player character during eval command execution.

function
&key name (rooms nil) (connections nil) (entrance nil)

Create a new empty area, optionally pre-populated with ROOMS (a list of mud-rooms), CONNECTIONS (a list of mud-connections, whose endpoint rooms are added automatically), and an optional ENTRANCE room. Returns the mud-area.

function
name session &key account
function
&KEY (NAME "a dusty guestbook") (FILEPATH NIL FILEPATH-SUPPLIED-P)
function
&key name description hp max-hp attack-min attack-max defeat-message victory-flag

Create a new NPC.

function
&KEY (NAME "object") (LOCATION NIL) (DESCRIPTION "") (ALIASES NIL) (KEYWORDS NIL) (PROPERTIES NIL)

Create a new MUD object. KEYWORDS control which body slots the object can be worn/held in (see wear and ITEM-FITS-SLOT-P). PROPERTIES is an optional plist of initial object properties (applied with object-set-property), e.g. a weapon's "damage-min"/"damage-max".

function
&KEY (NAME "A Room") (DESCRIPTION "")

Create a new room.

Create a new base mud-session with no backing I/O. Subclasses with I/O should be used instead (e.g. stream-session).

function
&KEY (NAME "a wooden sign") (DESCRIPTION "A simple wooden sign with words carved into it.") (MESSAGE "Nothing is written here.") (LOCATION NIL) (ALIASES NIL) (KEYWORDS NIL)

Create a new sign with a fixed MESSAGE that characters can read.

function
&KEY (NAME "a weapon") (DESCRIPTION "") (ALIASES NIL) (KEYWORDS NIL) (LOCATION NIL) (DAMAGE-MIN +WEAPON-DAMAGE-MIN+) (DAMAGE-MAX +WEAPON-DAMAGE-MAX+)

Create a weapon, ready to be held in a hand.

The "weapon" keyword is added automatically, so weapon-p recognises the object and it fits a hand limb; KEYWORDS are extra keywords for name matching. DAMAGE-MIN/DAMAGE-MAX default to the generic weapon range and are stored as the object's "damage-min"/"damage-max" properties.

function
&KEY (NAME "a Wordle puzzle board") (DESCRIPTION "A large wooden board with five-letter slots neatly arranged. Coloured pegs sit in trays beside it, ready to mark each guess.") TARGET-WORD (MAX-GUESSES 6) (WORD-LIST \*WORDLE-DEFAULT-WORDS\*) SEED

Create a new Wordle puzzle object.

TARGET-WORD is the 5-letter word to guess. If not provided, the daily word (based on today's date) is used. Each puzzle instance generates its own random SEED so different puzzles produce different daily words.

SEED can be provided explicitly to create reproducible puzzle instances.

Mark an NPC as defeated.

Bring a defeated NPC back to life at full hit points, clearing its defeated flag. Returns NPC. The inverse of NPC-DEFEAT!.

Find a single object in the current eval world by ID or partial name match.

If SPEC is an integer, looks up the object by world-level ID via world-object-by-id. If SPEC is a string, returns the first object whose name partially matches (via world-objects-matching), or NIL if none match.

Examples: (obj-find 42) ; Find object with world ID 42 (obj-find "guard") ; Find first object matching "guard"

Return the class/type name of OBJECT as a string. Useful when ~S printing is too verbose. Example: (obj-type (me))

Get a property value from an object, or NIL when unset.

Move an object to a new location.

Return non-NIL if NAME matches the object's primary name (exact or whole-word, case-insensitive) or any alias (exact, case-insensitive). Returns NIL for empty NAME.

Place CHARACTER in the world's starting room and return the character. The character must already be registered in the world (via CREATE-OBJECT! or WORLD-ADD-OBJECT!). This only sets location and room membership — it does not assign IDs or index into world tables.

function
world character command-string

Process a command from a character. Convenience wrapper around handle-command using the world's parser.

Return all properties of OBJECT as a string (from its properties FSET map). Example: (props (me))

Read a line from SESSION by polling with a short timeout (POLL-INTERVAL). If polling times out, it sends a keepalive heartbeat to verify the connection is still alive, then continues waiting. Returns (values line status).

Wrap TEXT in ANSI red foreground.

function
name password &key email admin

Register a new account with NAME and PASSWORD. Signals an error if an account with the same name already exists. EMAIL is optional and stored for password recovery. ADMIN, when true, marks the account as an administrator (allowing the owner's characters to use the in-game eval command). Returns the newly created mud-account.

Reload the APEIRON system and re-establish the eval context. Call this from the MUD via 'eval (reload-apeiron)' after modifying source files to pick up changes without restarting the server.

ASDF recompiles the files that changed (and everything that depends on them), so a plain QUICKLOAD is enough for normal git-based workflows; no forced recompile is needed.

After reloading, re-syncs APEIRON.EVAL with APEIRON.CORE. If CORE exported a symbol that a previous /eval form interned in the eval context (a defvar or defun, say), that local symbol would shadow the new export. Uninterning the shadows and re-establishing the :use makes fresh exports visible without manual intervention.

Return a random integer uniformly distributed across MIN, MAX.

Return a blocking message if the character cannot use this exit yet.

Four independent checks: 0. One-way — the connection cannot be traversed from this room 1. Regular block — the connection is blocked for everyone (locked door) 2. Challenge block — the connection requires a flag the character doesn't have (riddle) 3. Flag gate — the room requires a flag the character doesn't have (defeat NPC)

Return the list of mud-connections that lead out of ROOM.

When ROOM belongs to an area, the area's connections incident to ROOM are the preferred source (they live in the area, not on the room), unioned with the room's own CONNECTIONS list which holds cross-area and legacy connections. Rooms outside any area fall back to their own connections list. The result is deduplicated.

Return a list of (direction connection) for every usable exit in ROOM.

DIRECTION is a lowercase string, CONNECTION is the mud-connection. One-way connections are only listed from their passable end. Exits come from room-exit-connections (area connections first, then the room's own).

Get the target room when moving in DIRECTION from ROOM.

Returns the room at the other end of the matching Connection, or NIL. One-way connections only yield a target from their passable end.

Write all accounts to the accounts data file. Each account is serialized as a property list; the file contains one list of all account plists.

function
room exit-direction flag &optional message

Set a flag-based gate on ROOM: the EXIT-DIRECTION is blocked until the character has FLAG. MESSAGE is shown when they try to pass.

Shut down log4cl, flushing any buffered messages. Called from stop-mud-server.

Describe all slots of OBJECT, returning a string. Like CL:DESCRIBE but returns a string instead of printing to standard-output. Example: (slots-of (me))

Split INPUT into a command name and raw args string. Returns (values COMMAND-NAME RAW-ARGS-STRING). This is the raw building block the default parser uses; derived parsers can call it directly to reuse the plain first-token split.

Get the starting room of the world.

Return how many points a stat at LEVEL needs to reach LEVEL+1.

The requirements follow a Fibonacci progression: 10, 10, 20, 30, 50, 80, 130, ... for levels 10, 11, 12, 13, 14, 15, 16, ... respectively.

Return the list of synonyms for CONN from ROOM's perspective. CONN is a mud-connection, ROOM is a mud-room. Returns a list of strings (e.g. '("n") for north), or NIL.

Wrap TEXT in ANSI underline.

Return (values MIN MAX) for WEAPON's damage roll, reading its "damage-min"/"damage-max" properties and falling back to the generic weapon range when either is missing. MIN is never greater than MAX.

Return non-NIL if OBJECT is a weapon — i.e. it carries the "weapon" keyword (case-insensitive).

Wrap TEXT in ANSI white foreground.

Check if CHARACTER-NAME has failed the puzzle.

Get the list of guesses for CHARACTER-NAME.

Check if CHARACTER-NAME has solved the puzzle.

function
&key (word-list \*wordle-default-words\*) (universal-time (wordle-now)) (seed nil seed-p)

Return a deterministic word from WORD-LIST based on UNIVERSAL-TIME.

Same date always gives the same word; word changes daily. Uses hashing of the date to produce a pseudo-random (but stable) index, so consecutive days do not pick adjacent words.

When SEED is provided, it is combined with the date so different seeds produce different words on the same date. Each puzzle instance stores its own seed to vary its daily word independently.

function
&optional (universal-time (wordle-now))

Return an integer YYYYMMDD for UNIVERSAL-TIME, for date comparisons.

Display the Wordle puzzle state for CHARACTER-NAME.

Shows the board with all guesses and the remaining empty slots. When solved or failed, the result message shows a shareable pattern of UTF-8 emoji squares (no letters) so it can be pasted to other media without spoiling the answer.

function
puzzle &optional (universal-time (wordle-now))

If the puzzle's word is stale (different date than UNIVERSAL-TIME), rotate to today's daily word and clear all character state. Returns T if rotated, NIL if the word was already current.

Evaluate a guess against the target word.

Returns a list of 5 keyword results: :correct - letter is correct and in the right position :present - letter is in the word but wrong position :absent - letter is not in the word at all

Handles duplicate letters correctly: if a letter appears twice in the guess but only once in the target, only one gets :present and the other gets :absent.

Uses wordle-normalize for accent-insensitive comparison: accented characters in the target (from the word list) are compared against plain ASCII letters in the guess (as typed by the player), so e.g. 'maçã' matches 'macac'. The original characters of both target and guess are preserved for display.

Return a string displaying this PUZZLE's Wordle leaderboard. Sorted by correct guesses descending, then plays descending. Only accounts with at least one play are shown.

function
puzzle character-name guess-word

Process a guess from CHARACTER-NAME.

Returns multiple values: (values display-string result-code) where RESULT-CODE is one of: :solved - correct guess :continue - valid guess, keep going :failed - out of guesses :invalid - invalid word (wrong length, not letters) :already - already solved/failed this puzzle :repeat - already guessed this word

Return a help string explaining how to play Wordle on this puzzle.

Record a completed Wordle game for ACCOUNT-NAME on this PUZZLE.

RESULT-CODE is :solved or :failed. Increments the play count and optionally the correct count for this account on this puzzle.

Only registered accounts (non-guest) are tracked — guests are silently ignored. ACCOUNT-NAME is the mud-account name (not character name).

Return a copy of STRING with characters that bear diacritics replaced by their base letters (e.g. á→a, ç→c, ü→u). The case of the original character is preserved.

Uses CHAR-NAME to decompose, which handles any accented character generically — no explicit mapping table needed. Characters without diacritics are returned unchanged.

Reset the puzzle for all characters. Optionally set a NEW-WORD. When NEW-WORD is given the WORD-DATE is updated to today.

Reset the puzzle for a single character.

function
puzzle &optional (universal-time (get-universal-time))

Set the puzzle's target word to today's daily word and reset all character progress. Uses the puzzle's SEED so different puzzle instances get different daily words.

Assign a world-level ID to an object, register it in the world's indices, and return the object.

Return the list of all areas registered in the world.

Return all objects registered in the world.

Return all objects registered in the world.

Look up an area in the world by its world-level ID.

Return the area in WORLD that contains ROOM, or NIL.

Uses the room's room-area (1 2) back-reference when it points at an area registered in WORLD (the common case); otherwise falls back to scanning the world's areas. The one-area-per-room invariant (enforced by WORLD-ADD-AREA!) guarantees a room belongs to at most one area, so this is unambiguous.

Return the first area in WORLD with the given NAME (case-insensitive), or NIL.

function
world message &optional exclude-character

Broadcast a message to all characters (optionally excluding one).

Look up an object in the world by its world-level ID.

Return the first object in the world with the given NAME, or NIL.

Return a list of all objects in WORLD whose name or aliases match NAME. Matching is done via object-name-matches (case-insensitive, whole-word, and alias checks). Returns an empty list when no objects match.

Remove AREA from WORLD's indices (not its rooms or connections, which may be shared with other areas). Returns AREA.

Remove a character from the world. Account-owned characters (with a non-nil ACCOUNT) are displaced from their room but kept in world indices. Guest characters (no account) are completely removed: their worn and carried items are first dropped into the room (see DROP-CHARACTER-ITEMS!), then the character is removed from the indices and destroyed.

Bring every defeated NPC in WORLD back to life at full hit points, delegating to AREA-RESURRECT-NPCS! for each registered area. Returns the list of NPCs revived, or NIL when none were defeated.

Look up a room in the world by its world-level ID.

Return the number of areas registered in the world.

Count active (online) characters — those with a live session.

Return the number of rooms in the world.

Wrap TEXT in ANSI yellow foreground.

Macros

macro
name (world character args) &body body

Define a command handler. world is the mud-world instance, CHARACTER is the character, ARGS is a raw string that the handler can parse as needed.

Variables

variable
#<hash-table :test equal :count 0 {1204980883}>

Hash table of all registered accounts, keyed by account name (string).

When non-NIL, color-text and helper functions emit ANSI escape codes. Bind this to NIL to disable colors globally per-session or per-thread.

variable
#<hash-table :test equal :count 27 {1204980903}>

Hash table of command handlers

Bound to the player character whose command is currently being handled. Dynamic: when a command handler runs (see handle-command) it is set to the acting character, so code that runs during a player's command — including the in-game eval command — can tell which player caused an object to be created. CREATE-OBJECT! uses this to record the object's CREATOR. NIL outside any command context (world boot, login flow, tests that call CREATE-OBJECT! directly).

variable
#P"/home/runner/work/apeiron-mud/apeiron-mud/data/"

Directory for run-time data files (guestbook CSV, etc.).

Bound to the current player character during eval command execution.

Bound to the current character's location during eval command execution.

Bound to the current world during eval command execution.

T if configure-logging has already been called.

One of :PROD, :DEBUG, or :TEST. See logging.lisp for log behaviour.

variable
#("about" "above" "abuse" "actor" "acute" "admit" "adopt" "adult" "after" "again" "agent" "agree" "ahead" "alarm" "album" "alert" "alien" "align" "alive" "allow" "alone" "along" "alter" "angel" "anger" "angle" "angry" "apart" "apple" "apply" "arena" "argue" "arise" "aside" "asset" "avoid" "award" "aware" "awful" "bacon" "badge" "basic" "basis" "batch" "beach" "beard" "beast" "begin" "being" "below" "bench" "berry" "birth" "black" "blade" "blame" "blank" "blast" "bleed" "blend" "bless" "blind" "block" "blood" "board" "boast" "bonus" "booth" "bound" "brain" "brand" "brave" "bread" "break" "breed" "brief" "bring" "broad" "broke" "brook" "brown" "brush" "buddy" "build" "built" "bunch" "burst" "cabin" "candy" "cargo" "carry" "catch" "cause" "cedar" "chain" "chair" "chaos" "charm" "chart" "chase" "cheap" "check" "cheek" "chess" "chest" "chief" "child" "chill" "choir" "civil" "claim" "clash" "class" "clean" "clear" "clerk" "climb" "cling" "clock" "close" "cloth" "cloud" "coach" "coast" "coral" "count" "court" "cover" "crack" "craft" "crane" "crash" "crawl" "crazy" "cream" "crest" "crime" "crisp" "cross" "crowd" "crown" "crush" "cubic" "curve" "cycle" "daily" "dance" "debut" "decor" "depth" "derby" "desk" "dirty" "doubt" "dozen" "draft" "drain" "drama" "drank" "drawo" "dream" "dress" "dried" "drift" "drill" "drink" "drive" "drown" "drums" "dying" "eager" "eagle" "early" "earth" "eight" "elbow" "elder" "elect" "elite" "empty" "enemy" "enjoy" "enter" "entry" "equal" "equip" "error" "essay" "event" "every" "evict" "exact" "exile" "exist" "extra" "fable" "facet" "faith" "fancy" "fatal" "fault" "feast" "fence" "ferry" "fetch" "fever" "fiber" "field" "fierce" "fight" "final" "finch" "firm" "fixed" "flame" "flash" "fleet" "flesh" "float" "flock" "flood" "floor" "flora" "flour" "fluid" "flush" "flute" "focal" "focus" "force" "forge" "forth" "forum" "found" "frame" "frank" "fraud" "fresh" "front" "frost" "froze" "fruit" "fully" "gauge" "ghost" "giant" "given" "glass" "glide" "globe" "gloom" "glory" "gloss" "glove" "going" "grace" "grade" "grain" "grand" "grant" "grape" "grasp" "grass" "grave" "great" "green" "greet" "grief" "grill" "grind" "groan" "groom" "gross" "group" "grove" "guard" "guess" "guest" "guide" "guild" "guilt" "gully" "happy" "harsh" "haste" "haunt" "haven" "heart" "heavy" "hedge" "hence" "herbs" "hobby" "honey" "honor" "horse" "hotel" "house" "hover" "human" "humor" "hurry" "ideal" "image" "imply" "index" "indie" "infer" "inner" "input" "irony" "ivory" "jewel" "joint" "joker" "judge" "juice" "jumbo" "jumpy" "kebab" "knack" "kneel" "knife" "knock" "known" "label" "labor" "lance" "large" "laser" "later" "laugh" "layer" "learn" "lease" "leave" "legal" "lemon" "level" "lever" "light" "limit" "linen" "liver" "local" "logic" "loose" "lover" "lower" "loyal" "lucky" "lunar" "lunch" "lyric" "major" "maker" "manor" "maple" "march" "marry" "marsh" "match" "maybe" "mayor" "meant" "media" "mercy" "merge" "merit" "merry" "metal" "midst" "might" "minor" "minus" "mixed" "model" "money" "month" "moral" "motor" "mount" "mouse" "mouth" "movie" "music" "naive" "nanny" "nasty" "naval" "nerve" "never" "night" "noble" "noise" "north" "noted" "novel" "nurse" "nylon" "oasis" "occur" "ocean" "offer" "often" "olive" "onset" "opera" "orbit" "order" "organ" "other" "outer" "overt" "owner" "ozone" "paint" "panel" "panic" "paper" "party" "pasta" "patch" "pause" "peace" "pearl" "penny" "phase" "phone" "photo" "piano" "piece" "pilot" "pinch" "pitch" "pixel" "place" "plain" "plane" "plant" "plate" "plaza" "pluck" "plumb" "plume" "plump" "point" "polar" "pound" "power" "press" "price" "pride" "prime" "prince" "print" "prior" "prism" "prize" "probe" "proof" "prose" "proud" "prove" "pulse" "punch" "pupil" "purse" "queen" "query" "quest" "queue" "quick" "quiet" "quite" "quota" "quote" "radar" "radio" "rally" "ranch" "range" "rapid" "ratio" "reach" "react" "ready" "realm" "rebel" "refer" "reign" "relax" "reply" "rider" "ridge" "rifle" "right" "rigid" "risky" "rival" "river" "robot" "rocky" "rouge" "rough" "round" "route" "royal" "rugby" "ruler" "rural" "sadly" "sauce" "scale" "scare" "scene" "scent" "scope" "score" "sense" "serve" "setup" "seven" "shade" "shaft" "shake" "shall" "shame" "shape" "share" "shark" "sharp" "shave" "sheer" "sheet" "shelf" "shell" "shift" "shine" "shirt" "shock" "shore" "short" "shout" "shove" "sight" "sigma" "since" "sixth" "sixty" "skate" "skill" "skull" "slash" "sleep" "slice" "slide" "small" "smart" "smell" "smile" "smoke" "snack" "snake" "solar" "solid" "solve" "sorry" "sound" "south" "space" "spare" "spark" "speak" "speed" "spell" "spend" "spice" "spill" "spine" "spite" "split" "spoke" "sport" "spray" "squad" "stack" "staff" "stage" "stain" "stair" "stake" "stale" "stall" "stamp" "stand" "stark" "start" "state" "stays" "steal" "steam" "steel" "steep" "steer" "stern" "stick" "stiff" "still" "stock" "stone" "stood" "store" "storm" "story" "stove" "strap" "straw" "strip" "stuck" "study" "stuff" "style" "sugar" "suite" "sunny" "super" "surge" "swamp" "swear" "sweep" "sweet" "swept" "swift" "swing" "sword" "swore" "sworn" "syrup" "table" "taste" "teach" "teeth" "thank" "theft" "their" "theme" "there" "these" "thick" "thief" "thing" "think" "third" "thorn" "those" "three" "threw" "throw" "thumb" "tiger" "tight" "timer" "tired" "title" "toast" "today" "token" "total" "touch" "tough" "tower" "toxic" "trace" "track" "trade" "trail" "train" "trait" "trash" "treat" "trend" "trial" "tribe" "trick" "tried" "trip" "troop" "truck" "truly" "trump" "trunk" "trust" "truth" "tumor" "twice" "twist" "ultra" "uncle" "under" "unfair" "union" "unite" "unity" "until" "upper" "upset" "urban" "usage" "usual" "valid" "value" "valve" "vault" "verse" "video" "vigor" "vinyl" "viral" "virus" "visit" "vista" "vital" "vivid" "vocal" "vodka" "voice" "voter" "vouch" "wagon" "waist" "waste" "watch" "water" "weave" "wedge" "weigh" "weird" "whale" "wheat" "wheel" "where" "which" "while" "whine" "white" "whole" "whose" "wider" "witch" "woman" "world" "worry" "worse" "worst" "worth" "would" "wound" "wrath" "write" "wrong" "wrote" "yacht" "yield" "young" "youth" "zebra")

Default vector of 5-letter words for Wordle puzzles.

When non-NIL, overrides the time used by wordle-date-key and WORDLE-ENSURE-FRESH-WORD! for testing purposes.

variable
#("abrir" "acaso" "achar" "agudo" "altar" "amado" "amigo" "andar" "anexo" "apoio" "areia" "areal" "arroz" "asilo" "ativo" "atrás" "áudio" "aviso" "baixo" "banho" "barco" "batal" "beijo" "bicho" "bolsa" "bomba" "bônus" "braço" "bravo" "breve" "briga" "busca" "cabra" "cacho" "caixa" "calma" "calor" "campo" "capaz" "carne" "carro" "carta" "cesta" "chefe" "cheio" "choro" "chave" "ciclo" "cinza" "circo" "cobra" "colar" "comer" "copos" "corda" "corpo" "costa" "couro" "cravo" "credo" "crise" "cruza" "dança" "dados" "danos" "dedos" "deles" "doido" "donas" "dorso" "drama" "droga" "dever" "fácil" "falar" "farol" "falso" "fases" "feliz" "festa" "ficha" "filho" "filme" "final" "fogos" "folha" "fonte" "forma" "forte" "força" "fruta" "fugir" "fúria" "galho" "garfo" "gasto" "gelos" "gêmeo" "geral" "gesto" "gente" "goela" "golpe" "gosto" "grade" "grama" "graxa" "grilo" "grude" "grupo" "harpa" "haste" "homem" "honra" "hoste" "ideal" "igual" "irmão" "isola" "janta" "jogar" "juízo" "leite" "lento" "livro" "louco" "lugar" "luzir" "maçãs" "macho" "magro" "manto" "mares" "matar" "meiga" "melão" "menos" "mesmo" "metal" "minha" "misto" "mocho" "monge" "monta" "morte" "morar" "mundo" "mural" "murro" "naval" "navio" "negar" "ninho" "nível" "noite" "nossa" "nuvem" "óbito" "olhar" "olhos" "órgão" "palha" "pampa" "papel" "parar" "parte" "passo" "pátio" "pavio" "peixe" "pêlos" "pense" "perda" "pesca" "piano" "pilha" "pires" "pista" "pobre" "poder" "poema" "pompa" "ponte" "porta" "posse" "pouco" "praia" "prata" "prato" "prazo" "preço" "presa" "preto" "primo" "prosa" "pular" "pulso" "quase" "rádio" "raiva" "range" "rapaz" "raste" "reais" "recém" "redor" "regra" "reino" "reler" "remar" "rente" "resma" "resto" "rezar" "risco" "risos" "rocha" "rodar" "rolha" "rompa" "rosto" "roubo" "rubro" "rugir" "rumor" "sábio" "sabão" "sabor" "sadio" "salmo" "salto" "saúde" "seara" "seção" "seita" "selva" "selos" "sente" "serra" "servo" "sesta" "sexta" "sigla" "silvo" "sítio" "sobra" "sogra" "soldo" "solta" "sonho" "sopro" "sorte" "sulco" "sumir" "surdo" "surto" "tábua" "talho" "tanto" "tarde" "tarja" "tecer" "teima" "telha" "tempo" "tênis" "tenso" "terço" "termo" "terno" "terra" "testa" "teste" "texto" "tigre" "tinta" "tirar" "toada" "tocha" "tolos" "tomar" "tonto" "tórax" "torre" "torta" "tosse" "tosta" "total" "touro" "traço" "traga" "trama" "trapo" "trato" "trema" "trigo" "troca" "troço" "trono" "trova" "trupe" "tumba" "tumor" "turma" "turno" "ungir" "único" "unido" "untar" "urdir" "urgir" "usado" "usura" "vadio" "vagão" "vagar" "vapor" "varal" "vasto" "vazão" "vazio" "vedar" "veias" "velar" "velho" "veloz" "vemos" "vendo" "vento" "verão" "verbo" "verde" "verso" "veste" "vetar" "vidas" "vidro" "vigia" "vigor" "vinda" "vinho" "vinte" "viola" "virar" "visão" "visar" "visto" "vital" "viver" "vivos" "vogal" "volta" "vulto")

Vector of 5-letter Portuguese (pt-BR) words for Wordle puzzles. Includes accented characters (á, â, ã, ç, é, ê, í, ó, ô, õ, ú).

Level every character stat starts at.

A character's maximum HP is this many times their stamina level.

Highest level a character stat can reach.

Strength points gained for every attack a character makes.

Character levels of strength that add one point of melee damage.

Maximum damage of a bare-handed attack, before the strength bonus.

Minimum damage of a bare-handed attack, before the strength bonus.

Bold / increased intensity

Dim / decreased intensity

Italic (not widely supported)

Reverse / inverse video

Default maximum damage for a weapon that defines no damage range.

Default minimum damage for a weapon that defines no damage range.

APEIRON.CORE.UTILS

Functions

Format a message with proper line breaks.

function
format-string &rest args

Log an error message via log4cl. The message is routed through the configured log4cl appenders and always printed to the console for visibility.

Log an informational message via log4cl. The message is routed through the configured log4cl appenders (file and console, depending on configure-logging). All existing call sites continue to work unchanged.

Generate a unique ID for objects.

APEIRON.PERSISTENCE

Classes

PERSISTENT-AREA
PERSISTENT-CHARACTER
PERSISTENT-CONNECTION
PERSISTENT-DECORATOR
PERSISTENT-GUESTBOOK
PERSISTENT-OBJECT
PERSISTENT-ROOM
PERSISTENT-WORDLE
PERSISTENT-WORLD
WRAPPING-PERSISTENT-CLASS

A persistent-class that can safely inherit slots from a non-persistent standard-class parent.

Readers

List of slot names inherited from non-persistent parents that should be treated as transient (not stored in the datastore).

Accessors

List of slot names inherited from non-persistent parents that should be treated as transient (not stored in the datastore).

Generics

Convert OBJ into its persistent counterpart and register it with BKNR.

Dispatching on the class of OBJ allows adding new object types without modifying this generic function -- just add a defwrapping-persistent-class and optionally specialize materialize-object if extra steps are needed.

The persistent class is resolved through the global *PERSISTENT-CLASS-REGISTRY* (see TRANSIENT->PERSISTENT-CLASS).

Uses CHANGE-CLASS (preserving object identity and all cross-references) followed by INITIALIZE-INSTANCE to trigger BKNR registration.

Functions

Return the data migration version recorded in WORLD's config, tolerating the legacy hash-table config of a pre-FSET datastore. A datastore written before version markers existed has no :data-version key and reports 0.

Define the wrapping persistent class for TRANSIENT-NAME with OPTIONS.

STORE-OBJECT is appended to the superclasses so the class participates in the BKNR datastore; the wrapping-persistent-class metaclass turns the :TRANSIENT-SLOTS option into transient (non-persisted) slot definitions.

Define the wrapping persistent classes declared in REGISTRY.

REGISTRY is a serapeum dict mapping each transient game class name to an options dict:

(dict 'mud-room (dict :transient-slots '(contents)))

Supported options: :transient-slots — list of slot names inherited from the transient class that must NOT be stored in the datastore. :persistent-name — symbol for the persistent class; defaults to PERSISTENT- (mud-room → persistent-room). :superclasses — explicit superclass list; defaults to the transient class plus persistent-object when the transient class is a subtype of mud-object (the common case).

Entries may be listed in any order: dependencies between persistent classes (e.g. on persistent-object) are resolved automatically.

Return world instance persisted in bknr store

Return the highest migration version registered, or 0 if none.

Open the BKNR data store for MUD persistence. If the store is already open it is reused to avoid unnecessary close/reopen cycles that trigger BKNR transaction log replay warnings.

Reload all guestbook entries from their CSV files. Run this after restarting the server if guestbook entries look stale. Usage from the MUD: eval (refresh-guestbooks)

Add FN (a function of one argument, the persistent world) to the migration registry under VERSION. VERSION must be a positive integer.

The registry stays sorted by ascending VERSION. Re-registering an already-present VERSION replaces the old migration — this file is reloaded on every hot reload, so idempotent registration is required.

Apply every registered data migration with version greater than WORLD's current data version, in ascending order.

Runs each migration in its own transaction so a failing migration rolls back cleanly, then advances the recorded version marker and snapshots the datastore once at the end. Returns the list of migrations that ran (each as a DATA-MIGRATION), or NIL when nothing was pending.

Pull latest code changes into the running image safely.

Snapshots the datastore first as a safety baseline, so a broken reload can be rolled back. Then reload-apeiron reloads the changed APEIRON systems, which re-runs define-persistent-classes and redefines the persistent classes from *PERSISTENT-CLASS-REGISTRY* whenever that file changed.

After the reload, pending data migrations (see run-data-migrations) are applied. The runner is data-driven — it iterates the migration registry — so adding a new migration in the future never requires changing this function. (Note that a migration whose code ships in this very reload cannot be run by an OLD safe-update that is already loaded; that is why run-data-migrations also runs from world-restore-or-initialize whenever a datastore is opened by current code.)

A second snapshot is taken only when the persistent class schemas actually changed — the situation BKNR warns about ('class ~A has been changed ... please snapshot your datastore') — or when data migrations ran — so the new schema and migrated data are persisted. When neither happened, the first snapshot is still current and the redundant second snapshot is skipped.

Snapshot the datastore so all persistent objects are written to disk.

Return the persistent class that wraps TRANSIENT-CLASS.

The persistent class name is derived from TRANSIENT-CLASS's entry in *PERSISTENT-CLASS-REGISTRY* (the declarative data in registry.lisp) — no class-hierarchy walking needed.

function
&key force-new (initializer #'default-transient-world)

Restore the world from the BKNR datastore, or materialize a fresh one.

When no stored world is found, INITIALIZER (a function of no arguments that returns a transient mud-world) is called to produce the transient world, which is then materialized into persistence. Defaults to DEFAULT-TRANSIENT-WORLD.

When FORCE-NEW is true any existing store data is wiped first.

An existing datastore is upgraded to the current data format by run-data-migrations before guest cleanup or index population, so migrated slot values are authoritative for everything downstream (e.g. account-owned characters whose data was written under an older slot name are not mistaken for guests and dropped). A fresh world is stamped with the latest migration version at creation.

Macros

macro
version name (world-var) &body body

Define a data migration with VERSION and NAME. BODY runs with WORLD-VAR bound to the persistent world object while the datastore is open; it may read and write BKNR store objects and the world config.

VERSION is a positive integer that must be greater than every previously registered migration's version. NAME is a human-readable string used in log output.

Adding a new migration later only requires another define-data-migration form — no changes to safe-update or the restore path.

macro
class superclasses slots &rest class-options

Like BKNR.DATASTORE:DEFPERSISTENT-CLASS but uses wrapping-persistent-class as the metaclass, so SUPERCLASSES can include vanilla STANDARD-CLASS parents whose slots lack :TRANSIENT annotations. STORE-OBJECT is automatically prepended to the superclass list.

Class option :transient-slots — list of slot names inherited from non-persistent parents that should NOT be stored in the datastore. All other inherited slots are persistent by default.

Example: (defwrapping-persistent-class my-persistent (my-class) () (:transient-slots name cached-data))

Variables

variable
#P"/home/runner/work/apeiron-mud/apeiron-mud/bknr/"

Directory for the BKNR data store. Bound to a temp dir during tests.