I’ve recently been getting back into using my computer more, which mostly means dealing with pieces of information. I was thinking about it and, boiled down, there’s two ways I do this:
In this post I’d like to explain how things (in-game objects) work. (This will also, incidentally, be a short tutorial on lambda calculus and closures.)
In the language of game design, Racket-MUD uses a type “entity component system,” more or less. I call it quality of things, and developed it mostly in the isolation of solitary hacking, so there are some differences.
Everything a user interacts with inside Racket-MUD is a thing, and every thing has qualities, which define its capabilities.
From the user’s perspective, a thing is a loaf of bread, a field, a starship, or even abstract things like jealousy: it all depends on the sort of MUD being implemented.
From the engine’s perspective, a thing is a function, into which functions are thrown, and out of which comes a function.
Let’s forget about things for a moment and look at some more generic code.
(define alef 7)
What’s this do? It defines
7. Simple enough.
(define bet (λ () 7))
What does this one do? It defines
bet as a function, that returns
(λ () 7) breaks down to “make a function (
λ) that takes zero arguments (
()) and returns
λ symbol? That’s the lowercase “lambda” character from the Greek alphabet; it’s used in a few programming languages (and branches of mathematics) to mean “a function.”
alef ; -> 7 bet ; -> <#procedure:bet> (bet) ; -> 7
7, but calling
bet returns a procedure – so, we have to call that procedure, by doing
(bet): and now we get our
Pretty simple. Let’s build on this.
(define gimel (λ () (define num 7) (λ () num)))
Less simple: there are two of those
λ things. We know that calling
gimel will return a function: let’s look at what that function actually would contain:
(define num 7) (λ () num)
So, first, it’s defining
7 and then… it returns a function, that we can see, takes no arguments, and returns
num. So how do we use
gimel ; -> <#procedure:gimel> (gimel) ; -> <#procedure> ((gimel)) ; -> 7
Well, we’ve certainly added complexity, but there hasn’t been any new capabilities added.
(define dalet (λ () (define num 7) (λ () (+ 1 num))))
Here, we’ve replaced returning num with returning
(+ 1 num).
So, running these we get:
dalet ; <#procedure:dalet> (dalet) ; <#procedure> ((dalet)) ; 8
Manipulating Enclosed Variables
Now we’re getting somewhere. Let’s add some more capabilities. Here, we’re going to make it so that the procedure returned by the outer-function is capable of taking an argument; that’ll be added (
(define he (λ () (define num 7) (λ (x) (+ x num))))
So now we can do:
he ; <#procedure:he> (he) ; <#procedure> ((he) 1) ; 8 ((he) 10) ; 17
Now that’s something! Let’s rename the function we’ve been working on to make what it does more clear.
(define add-to-seven (λ () (define seven 7) (λ (x) (+ x seven))))
Maybe now the point of why I’ve done this will be a little more clear: we’ve created a bit of data,
seven, and it’s held inside the function. Basically, all we can do is ask the function, “what would happen if we added x to 7?”
Let’s make it a bit more general.
Passing Lambdas Around
(define do-to-seven (λ () (define seven 7) (λ (f) (f seven))))
We’ve changed the returned function here: now it doesn’t take
x and return the result of
(+ x seven). It takes
f and returns the result of
You might be able to infer that
f is intended to be a function, not a number, and that it needs to take one argument, and that argument will be seven. Let’s play around with this.
do-to-seven ; <#procedure:do-to-seven> (do-to-seven) ; <#procedure>
Y’know these nested parantheses aren’t always that easy to read. Let’s switch up the style a bit.
(define seven-doer (do-to-seven)) seven-doer ; <#procedure>
Alright – now we can directly access the enclosed function,
(λ (f) (f seven)), with
seven-doer. Let’s move on.
(seven-doer (λ (S) S)) ; -> 7
What does this do? It calls
seven-doer, passing it a lambda function that takes one argument,
S, and returns
S. This returns
7. Let’s see how, by looking how the statement expands. First, the code above turns into, more-or-less:
((λ (f) (f seven)) (λ (S) S))
Ew. Hard to read, but try to follow the parantheses. The whole thing is one statement, with two parts: the first is a lambda function that takes
f and returns
(f seven), and the second is a lambda function that takes
S and returns
The first part of the statement is treated as a function, and the second part is its argument, so the next step from here is to pass the second part into the first part. We get to
((λ (S) S) seven)
(f seven) when
(λ (S) S).)
So, from here, we pass
seven into the lambda function, which just returns whatever
seven is, in this case,
Phew! That was a lot. The point is, now, we have a function that we can ask, “Hey, show me what would happen if I do the following to 7…“
…and “the following” can be just about anything you think of. Like, say, doubling it.
(define double (λ (x) (* x 2))) ; -> <#procedure:double> (do-to-seven double) ; -> 14
Now, this is a lot of semantic overhead for what is essentially
(* 7 2). but let’s see how this relates to Racket-MUD‘s things.
(define thing-maker (λ (name) (let ([thing (make-hash (list (cons 'name name)))]) (λ (f) (f thing)))))
(This isn’t actually the code for making a thing, just a simple version for explaining the concepts.)
Let’s pull this apart:
thing-maker is a function that takes one argument,
name, creates a hash-table called
thing to store the name, and returns a function that takes one argument,
f, and returns the result of
(define bob (thing-maker "Bob")) ; <#procedure> (bob (λ (thing) (hash-ref thing 'name))) ; "Bob"
So this defines
bob as the result of
(thing-maker "Bob"): a procedure,
(λ (f) (thing f), where
thing is a a hash-table of the key
'name to the value
bob, the variable, is something into which we can throw queries about Bob, the thing.
So if we give the function
(λ (thing) (hash-ref thing 'name))) to
bob – or any thing – we’re basically asking that thing, “Hey, what’s your name?”
And the thing will look at itself, and say what it’s name is –
In this way, we’re able to arbitrarily define new qualities for the thing (by adding them to its hash-table), or manipulate them in new wyas, without having to extend the core functionality of the thing, which is, essentially, just making a hash-table and returning
(λ (f) (f hash-table)).
And that’s how things work!
In this post I’d like to explain how the
(actions) event works. It grew out of the “Actions Service,” which is kind of a defunct concept now that Racket-MUD has gotten more functional.
Services were features loaded into the MUD as it loaded, and some of them provided
(tick) functions which were added to a list and, as you might guess, tick’d every, well, tick.
Now, there’s no real “services” as a distinct feature: just a list of events which are passed to the the MUD when it’s started. At the moment, the Teraum MUD is started by the following statement:
(run-engine (make-engine "Teraum" (list (accounts) (actions) (mudmap teraum-map) (mudsocket) (talker))))
(make-engine) function takes a name, and then a sequence of functions. The one we’ll be looking at this time is the second in that list –
The reason we add
(actions) to the list instead of just
actions is because we actually want to add the result of calling the procedure to the list of initial events, not the procedure itself.
So, what is the result of calling the
(define (actions) (define actions (make-hash)) (define (load state) (let ([schedule...]))) (define (tick state) (let ([schedule...]))) load)
I’ve cut out the contents of the two procedure definitions that live inside the
actions procedure, of
tick, for now, to keep things more readable.
We can see that when
(actions) is called, it defines a variable inside itself,
actions, as an empty hash-table, and then defines the two procedures I just mentioned, and then returns one of them, the load procedure.
So, calling the
actions returns another procedure,
load procedure is what’s scheduled as an event. Then, when Racket-MUD is started and begins ticking, it is called – and is passed the MUD’s
(scheduler . state) pair. We’re ready to look at what the
load procedure here actually contains, then.
Again, I’m going to simplify it.
(define (load state) (let ([schedule (car state)] [mud (cdr state)]) (hash-set! (mud-hooks mud) 'apply-actions-quality (λ (thing)...)) (schedule tick) state))
So, what is it that’s happening when this event is called?
First, it looks at the
state it is passed and breaks it into its constituent components: a
schedule procedure for adding new events to be called in the next tick, and the
mud data structure, which contains the current state of the MUD: name, scheduled events, extant things, and hooks.
Hooks are procedure that are available across the engine, and serve as kind of a generalized key-value database of functions. Think of it like a table:
|`apply-actions-quality`||`(λ (thing) (map (λ…`|
|`broadcast`||`(λ (chan speaker message)…`|
|`move`||`(λ (mover destination)…`|
So, this event, this
load procedure returned by calling
(actions), adds the
apply-actions=quality hook. We might end up looking at that hook’s procedure in another explainer. Then, it schedules the
tick procedure defined when we first called
(actions), and returns the MUD’s new state, now that it contains the
So in general, for any givenevent, the MUD’s
(scheduler . state) pair goes in, gets changed about, and then returned.
In this case, the MUD’s state gets a hook added to it, and another event gets scheduled: tick.
After this, the other events scheduled for the engine’s first tick happen, and then the engine moves on to the second tick, eventually coming to the event that
Here’s an abbreviated rendering of that procedure:
(define (tick state) (let ([schedule (car state)] [mud (cdr state)] [triggered (list)]) (hash-map actions (λ (chance records)...)) (schedule tick) state))
As you can see, the beginning and end of this procedure are nearly the same as
load was: it breaks up the MUD’s
(scheduler . state) pair into its constituents, and then later, after doing some sutff, schedules itself,
tick, and then returns the new state.
(scheduler . state) pair takes a while to type out. You might also see it written as the lowercase theta, θ, with an uppercase delta, Δ, used to represent schedule and a lowercase psi, ψ, for state. But not always -it depends on who wrote the code and documentation.)
Anyway! This brings us to the one thing about the
actions procedure that’s relatively unique, and handles what it actually does: what it does each tick.
It does two basic things:
- First it looks through every known action, a hash-table of actions added when the
apply-actions-qualityhook is called. a) For every record, it generates a random number between 0 and 10,000. If that number is… greater-than? less-than? Whichever makes sense, then the event is triggered: added to a list of triggered events.
- Second, for every triggered event, handle its task (the actual… action-y part of the action: not the actor, and not the chance, but the thing that happens.) b) If the task is a string, and the actor is in some sort of environment, send the string to every client in that same environment. c) If the task is a procedure, call it, passing it the actor.
That’s it! That’s how those room chats y’all who’ve played the demo come to love work. For completeness, here’s the actual statements that handle triggering and calling tasks, but it relies on procedures defined deeper in the engine, which haven’t yet been explained:
(hash-map actions (λ (chance records) (for-each (λ (record) (when (<= (random 10000) chance) (set! triggered (append (list record) triggered)))) records))) (for-each (λ (action) (let* ([actor (first action)] [task (third action)] [actor-quality (quality-getter actor)] [actor-location (actor-quality 'location)] [actor-exits (actor-quality 'exits)]) (cond [(string? task) ;send to things in th environment (let* ([environment (cond [actor-location actor-location] [actor-exits actor])]) (when (procedure? environment) (let ([environment-contents ((quality-getter environment) 'contents)]) (for-each (λ (thing) (((string-quality-appender thing) 'client-out) task)) (things-with-quality environment-contents 'client-out))))) ] [(procedure? task) (task actor)]))) triggered)
In this post I’d like to explain the “MUD stack” behind Teraum, what each one is as a project and how they relate.
Teraum is a MUD server running the Racket-MUD engine loaded with the RPG-Basics library and a custom map.
Phew! Lot of terms.
- A MUD server is a single MUD, running somewhere. Folk can connect to it, create accounts, and do whatever that server allows.
- A MUD engine is a piece of software that provides the game engine for running a MUD server. It usually doesn’t come with too many game-like features itself, usually focused around providing connectivity and utilities.
- A MUD library is a collection of code meant to be added into a *MUD engine to extend its features.
There’s our generic terms.
- Racket-MUD is a MUD engine that’s made by the same people who are making Teraum. It’s written in the Racket programming language. It’s about two hours older than the Teraum MUD, so is also in the very early stages of development.
- RPG-Basics is a MUD library that’s being made by us same folk who are making Teraum and Racket-MUD. It’s the first library being made for Racket-MUD, and as such, its code isn’t well-segregated from the engine.
Under-the-hood, the engine is just a procedure for handling the scheduling and sequential calling of events, another type of procedure for manipulating the state of the game world. When the engine is
made, it’s usually passed a list of events to call when it first starts. For example, Racket-MUD comes bundled with events for creating, loading, and saving user accounts, managing intra-engine user chat channels, and running the socket server to which those users will connect.
A library is a similar bundle of events. The RPG-Basics library currently provides events that load the world map and set up the ability for those created things to randomly act.
In practice, starting the MUD is done with the following line of Racket:
(run-engine (make-engine "Teraum" (list (accounts) (mudsocket) (talker) (actions) (mudmap teraum-map))))
As Teraum increases in sophistication, the number of events passed when the engine is
made will steadily increase, but the basic concept is there: pass a list of procedures to the engine to serve as the first events.
In follow-up posts, I plan on explaining how each of these events work, and providing a more thorough explanation of what
making an engine actually means. Also, what goes into the
teraum-map: how are in-game things represented, as source code.
Teraum is a tragically funny fantasy multiplayer role-playing game, played through text: players type commands and receive text output in response:
> look [Crossed Candles Inn] This is the Crossed Candles Inn: a simple wooden shack with several shuttered windows. There are a few wooden cots in one corner, and a long table which serves as the bar in another. There are some people seated at the table. A door leads out to Arathel County. Contents: emsenn and Lexandra Terr Exits: out > move out You attempt to move out [Turnwise Road, South Arathel, Greater Ack Metropolitan Enclosure, outside Crossed Candles Inn] This is an area of the Turnwise Road, a circular road...
(This type of text-based MMORPG is called a MUD.)
This blog will be where we (the development team) share updates about the game’s progress, until we have a blog set up within the MUD.
In this first post, we’d like to explain to you a little bit about the project, our plans for its future, and how y’all might get involved if you’re interested.
Teraum is a fantasy setting created by emsenn in the early 2000s, when they were a child. Since then it’s been the setting of various short stories, tabletop RPG campaigns, and even a podcast. It’s a world that used to have magic, but now doesn’t: it’s probably the most mundane fantasy setting you’ll ever learn about.
Teraum is currently being implemented as a MUD, or multi-user dimension: a text-based multiplayer online role-playing game. In it, players can become a human living on Teraum, in approximately the 8th decade after the Break(, when magic went away.)
The MUD is being implemented using a custom MUD engine being written from the ground-up in Racket. While Teraum’s source code is closed, so people have to actually play the game, the MUD engine’s source is available.
Right now, the MUD doesn’t have very many features. There are a bit more than fifty rooms you can walk around, representing some areas of the Old World, mostly centered around the Green Delta. That’s about it: you can walk around.
In the immediate future, most of the development team’s focus will be on developing the engine’s features, while we plan what gameplay within the MUD might look like. (Adding colourful output, properly handling unicode, stuff like that.)
We want gameplay in Teraum to be meaningful, even bordering on moralistic. Conflict and combat aren’t going to be prioritized; the latter might not ever be implemented. Instead, the gameplay is going to try and pit players against a turbulent game world, where the actions of things out of their control force them to perpetually adapt.
…The apples from Wultha never arrived. The Red Union puts out a notice, and players head to the the small remote town to
investigate. It has been burnt down. Commercial interests begin to reconstruct the town. A small boom occurs along the route between those interests and the town. Players find opportunities for work: importing and exporting, helping
repair, widen roads. Find their place in the situation, only for it to change again.
The world will not just need
analysis. Seasonal goods collected, roofs repaired before the spring showers come. Life is hard, even when no one’s making it that way, but that doesn’t make life bad.
How can you get involved? Teraum is looking for developers to help work on the Racket-MUD engine, as well as writers to help implement game areas. We’re not yet ready for playtesters, but we’ll be wanting them when we are!
If you want to get involved now, please send an email to firstname.lastname@example.org.