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
SBCL2.0+Quicklisp
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.lispYou should see:
[INFO] Initializing world...
[INFO] World initialized with 2 rooms
[INFO] MUD Server started on 127.0.0.1:8888Connect as a Player
In another terminal:
telnet localhost 8888Example Session
Note: the in-game
evalcommand is restricted. Only characters owned by an admin account, or characters wearing an object with both thehatandwizardkeywords (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
Persistent Objects — game objects are persisted and changes are logged to enable recovery (using
BKNR.Datastore).All power to the user — you can eval Lisp code directly within the game (could/should be restricted to admins in the future).
Hot Reloading — no need to ever shut the server down for maintenance (
WIP).
Protocols
Telnet (
RFC854) — network protocol for player connections, with option negotiation, line editing and echo control (seeapeiron/telnet).MSSP—MUDServer Status Protocol: advertises server details (name, players, game type, ...) to directory services.GMCP— Generic Mud Communication Protocol (telnet option 201): structured out-of-band client data. The server pushes character stats asChar.Vitals(hp/maxhp) andChar.Stats(str/sta/int). The protocol engine lives inapeiron/telnetand is decoupled from the game — the mapping from characters toGMCPpackages is done in the server bridge (session-sync-character).TLS— secure transport for telnet connections (viacl+ssl).ANSISGRcolors — colour output for the client (toggle withtoggle-colors).
Features
Currently Implemented
In-world
REPL— execute Lisp code from within the game (at your own risk, no guardrails)Multi-player networking — multiple players connect via telnet simultaneously
Object-oriented world — everything is an object with unique
IDs and extensible propertiesPersistence — objects are persisted through the
BKNRdatastore; journaling enables recovery in case the server needs to be shut downHot reloading — reload changed code into a running server without restarting (
reload-apeiron,safe-update)Room system — navigable rooms with directional exits (north, south, east, west)
Player chat —
saycommand for in-room communicationInventory system — foundation for item management
Command system — 17 built-in commands, easy to add more
Planned
Full item system — items with properties (take, drop, examine)
NPCsupport — non-player characters with behaviorsLLMNPCs —NPCs backed byLLMs, armed withMCPservers
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") ; → TBuilding 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.lispWorld
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.
Assign a world-level ID to an object, register it in the world's
indices, and return the object.
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.
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.
Hash table of command handlers
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.
Process a command from a character.
Convenience wrapper around handle-command using the world's parser.
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.
Hash table of command handlers
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.
Process a command from a character.
Convenience wrapper around handle-command using the world's parser.
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:
*data-migrations*— a registry ofDATA-MIGRATIONobjects ordered by ascendingVERSION.define-data-migration/register-data-migration— add a migration to the registry. Adding a future migration is purely additive: register a new migration with the next version number and it will run automatically — no changes tosafe-update,world-restore-or-initialize, or anything else are needed.current-data-version— the migration version recorded in the datastore (stored in the world's config under:data-version). A datastore written before this mechanism exists has no such key and reads as version 0, so every migration runs in order on the next open.run-data-migrations— apply all registered migrations whose version is greater than the datastore's current version, in ascending order, snapshotting once at the end so the migrated data and the new version marker are durable.
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).
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.
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 lengthServer 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
usocket — network communication
bordeaux-threads — multi-threading
flexi-streams — stream encoding
cl+ssl —
TLSsupportironclad — cryptography (password hashing)
log4cl — logging
str — string utilities
cl-csv — guestbook
CSVpersistencecl-graph — area graph algorithms
deeds — event system
bknr.datastore — persistence
serapeum — utility hash tables (the declarative persistent class registry)
fiveam — testing (optional)
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.
CL-GRAPH undirected graph whose vertices are mud-rooms
and whose edges carry the mud-connection as their element.
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.
CL-GRAPH undirected graph whose vertices are mud-rooms
and whose edges carry the mud-connection as their element.
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.
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.
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
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
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
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
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.
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
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
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.
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.
Accessors
The mud-area this room belongs to, or NIL when the
room is not part of any area.
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
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.
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.
Integer date key (YYYYMMDD) of the current target-word.
Vector of valid words for random selection.
Accessors
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.
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.
Integer date key (YYYYMMDD) of the current target-word.
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
All areas in the world, keyed by world-level ID.
Stores all online/active characters in world
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.
All objects in the world, keyed by world-level ID.
Command parser used to turn player input into command name and args.
All rooms in the world, keyed by world-level ID.
Accessors
All areas in the world, keyed by world-level ID.
Stores all online/active characters in world
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.
All objects in the world, keyed by world-level ID.
Command parser used to turn player input into command name and args.
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.
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.
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.
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.
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.
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")
Announce CHARACTER moving from FROM-ROOM to TO-ROOM in DIRECTION
to the other characters present.
Characters left behind in FROM-ROOM are told "TO-ROOM are told "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.
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.
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.
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 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.
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.
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.
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.
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+)
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
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.
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.
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))
Issue a character-input-event for the given session and input line.
Issue a character-output-event for the given session and output text.
Issue an error message as a Deeds error-event.
Issue an informational message as a Deeds info-event.
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.
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.
Create a limb. KEYWORDS are the item keywords allowed in it.
Return the current player character during eval command execution.
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.
Create a new NPC.
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".
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).
Create a new sign with a fixed MESSAGE that characters can read.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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
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.
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).
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.
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.
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.
Slow blink
Bold / increased intensity
Dim / decreased intensity
Italic (not widely supported)
Reset all attributes
Reverse / inverse video
Underline
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.
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.
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
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.
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
Directory for the BKNR data store. Bound to a temp dir during tests.