ToyMUD V1.0 - Documentation Chris Gray, January 1993 ToyMUD began as my first test of trying to use sockets under Sun UNIX. It grew into a project to see how little code is needed to write a reasonably general, fully-programmable MUD. The result is a server in about 3500 lines of C code. It is not terribly efficient, but it is quite powerful. Most of that power comes from the built-in programming language - the rest of the server only provides the database and client setup and communications. To get the system up and running, edit 'mud.h' to change the path to the default database. You may also want to change the default port. Then compile ('make'). Go to the directory you have put the database in, run the server in the background ('server &'), then run as many clients as you want. Just use the standard UNIX program 'telnet' as the client - you need to give it the port number you are using for the MUD server, however. To shut the system down, send a break to the server - it will shut down after all clients have gone away. Windows NT note: the server is called 'ToyMUD.exe', rather than 'server', and it will not go away right away when you type a control-C at it, because of the way Windows NT does signals. Also, the telnet client that comes with Windows NT doesn't work very well with ToyMUD, since it only supports character-mode, with no end-of-line options, and ToyMUD only works with full line-mode clients. It would probably be the same under Windows 95, but I don't have access to such a setup to try it out. As shipped, the system comes with a simple adventure-style scenario. Feel free to replace and/or mangle this as desired. I have written a number of compilers and interpreters in the past, but this is the first one where I did not build an internal data structure (tree) of the program and interpret from that. Thus, it may be a bit strange, but it seems to work OK. It is quite slow because of this style, and most errors are not detected until run-time. Error reporting is fairly poor, but trial and error (and likely a few debug prints) can normally find any problems. Using the Server The server program's usage line is: server [port [path]] 'port' is the port number to use on the socket connection, and defaults to whatever you have put in 'mud.h', normally 6666. 'path' is the access path to the database file, and defaults to whatever you have in 'mud.h'. When the player is fully connected, if the input line begins with a period, then the entire line, without the period, is treated as input to the programming language interpreter instead of being passed to the scenario's command parsing routine. Thus, there is no concept of a 'wizard' in ToyMUD - everyone is a wizard. It would not be difficult to add such a distinction to the system. Input lines received by the server are examined for a simple line-continuation system. If an input line ends with a '+', then the '+' is deleted and the line is not processed. Instead, it is saved in the server and successive input lines will be appended to the end of it. This will continue up to the server's line length limit, which is currently 4000 characters. This is useful when doing on-line building, since longer strings can be entered. E.g. .location(me()).desc := + "This is the new long description for this here room. It could be fairly long + and spread over several input lines." Ending a line in '-' is similar, except that the '-' is replaced by a newline and the next line is glued on after that newline. This is useful when interactively entering code, etc. in that you can explicitly enter the format you want them stored in. E.g. entering the following prime number program: .primes := `(max) - cprint(me(), "2 "); - cprint(me(), "3 "); - a := 5; - while a <= max do - b := 3; - p := 1; - while b * b <= a do - if a = b * b / a then - p := 0; - b := a; - fi; - b := b + 2; - od; - if p then - cprint(me(), itos(a) + " "); - fi; - a := a + 2; - od; - cprint(me(), "\n")` which can then be called by e.g. .primes(1000) Entering just .primes will display the contents of the 'primes' function, which is the text of it as entered, with the '-'s removed. Note that in the previous example of assigning to the current room's 'desc', you would likely want to use a '-' at the end of the first line of the string, so that there is an actual newline placed in the string. Connecting as a Client Using 'telnet', you can connect to the ToyMUD server using a command of the form telnet [hostname] port where 'port' is the port you have the server running on (default is 6666), and 'hostname' is the name (or internet address) of the machine running the server (default is the local machine). Note that ToyMUD currently only supports line-mode clients. Basic Concepts Used in Programming and Building The interpreter provides a number of builtin functions which are used to create things, find things, retrieve and set special attributes on players, produce output, parse input, etc. These are described fully later, but a couple will be mentioned here. The basic entity in ToyMUD is a "thing". This is basically a context (a set of attribute-value pairs) which is maintained by the server and can be stored in the database file. Each thing must have a unique name, so that the graph of them can be read from and written to the database file in a computable order. The 'thing' builtin, which generates new things at runtime, will generate a name if required. Each room or object in ToyMUD is just a thing. There is a thing associated with each player, to hold information about that player. The name of that thing is just the player's name. There is also a single global thing that serves as a global programming environment, and contains some special server-called functions described below. Property values (and hence variables and parameters) can be of the following types: int - signed integer (an 'int' in the C code of the host machine) string - a sequence of characters - string constants look like those in C proc - the same as a string, but in a standardized form that the interpreter understands. Represented as a sequence of characters surrounded by back-quotes (`) thing - a pointer to another thing Type 'void' also exists - it represents the concept of "no value". The thing value 'nil' represents a pointer to no other thing. The builtin 'describe' takes a thing as argument and will dump it out to the current client. E.g. here is the output for a room in the supplied scenario: .describe(location(me())) thing r_entrance: east ==> west ==> north ==> name ==> "in the entrance" desc ==> "The main door is to the south, and other doors lead north, east and west." south ==> out ==> contents ==> nil scenery ==> "doors door" Builtin 'me' returns the thing for the current client, and builtin 'location' returns the current room thing for a client corresponding to the passed thing. In the example output, the thing is named 'r_entrance', and has properties named 'east', 'west', 'north', 'name', 'desc', 'south', 'out', 'contents' and 'scenery'. By convention, every room in this scenario has properties 'name' and 'contents'. Similarly, every object has properties 'name' and 'next' (a pointer to the next object in any list). The interpreter is "dynamically typed" and does not use true declarations. Whenever a symbol is referenced, it is searched for in the context of the current function call (if any), and then in the global context. If it is not found, an error message is printed. When a symbol is assigned to, any already existing definition in the current call context or the global context will be reused, and the reuse must use the same type of value as the already existing definition. If the symbol being assigned to does not exist, then it is created in the current function call context. The '.' syntax can be used to explicitly reference or assign to a property in a context. Machines Machines are similar to "robots" or "bots" in other MUDS. In ToyMUD, they will normally run right in the MUD, rather than being an external program. This gives them full access to the data structures of the scenario. A machine is in most respects indistinguishable from a player. It should have whatever standard properties the scenario gives to players. The 'forEachPlayer' builtin will call its parameter for machines as well as for players. Whenever the server is restarted, a property named 'MACHINE_RESTART_ACTION' is looked for on each machine. If it exists and is a proc, it will be called. This is how machines can be operational on a server startup without any intervention. This routine will normally use the 'after' builtin to reschedule itself at a later time, thus allowing the machine to perform actions over time, much like a player does. Whenever the scenario does an 'aprint' (sending a message to all players in the same location as the passed entity), every machine in that same room will have it's 'LISTEN_ACTION' (if any) called with the printed string as its parameter. This is how machines can listen to and observe what is happening around them, and react to those events. Server Conventions The server will magically call global interpreted routines as follows: NEW_PLAYER_ACTION(player) - this is called, with the active player context as the argument, whenever a new player enters the game. This is a player who has never played before. This routine will typically do appropriate initialization of the player context and move the player to the start location. RESTART_ACTION(player) - this is called when a previously seen player re-enters the game. It will typically do a 'look around'. QUIT_ACTION(player) - this is called when a player is leaving the game, either as a result of the code calling the builtin 'quit', or as a result of the player entering an end-of-file or killing the client. PARSE_ACTION(string) - this is called with normal input lines from the player. The entire input line is passed in a string argument. This routine should "parse" the input command and execute it. The Programming Language The ToyMUD programming language doesn't really have a name, so I'll just call it 'TM'. It is not a C-like language, but resembles languages I have developed for other purposes - that is simply my own preference. TM is very minimal. It has no I/O (builtins are used), no logical operators, no goto (hurray!), continue, break or return, no unary negation (use subtraction from 0), etc. It is quite usable however. Those interested should be able to easily add things like logical operators, unary negation, etc. Gotos would be much harder, so don't try! Basic values: integer-constant [e.g. 1397] string-constant [e.g. "Hello there world!\n"] proc-constant [e.g. `(a, b) a * 2 + b * 5`] function-parameter-name variable-name [this includes functions] context.prop-name [property name fixed] context.{string-expr} [property name determined at run-time] {string-expr} [kind-of allows read-only pointers] proc-expr(proc-parameters) [can yield void or any type] The various forms can be combined arbitrarily. Property selection and procedure calling have the same precedence, so th.action(args) == (th.action)(args) thf(args).prop == (thf(args)).prop More complex uses are possible, but are likely hard to read. E.g. th.func(args1)(args2).{"p" + p1 + "_"}.p2(args3).p3 Assignment statements: variable := expression context.prop-name := expression context.{string-expr} := expression Precedence and combinations are as with basic values. Sequence expression: statement; statement ... statement; expression Sequence statement: statement; statement ... statement The forms 'sequence expression' and 'sequence statement' are identical. They are separated out since some constructs require void values. If construct (statement or expression): if then {elif then } [else ] fi An 'if' construct can be either a statement or expression, depending on the type returned. A result type of 'void' yields an 'if' statement. The various alternatives in an 'if' should all yield the same type, but this is not checked, so strange abuses are possible. An 'if' consists of one 'if' part, zero or more 'elif' parts, an optional 'else' part and a 'fi'. TM has no boolean type, but the supplied scenario provides global variables 'false' and 'true' with values 0 and 1. Note that TM has no boolean 'and', 'or' or 'not' operators, so if these are needed, they must be constructed out of 'if's. e.g.: if a < b then a else b fi if flag1 then statements1 elif flag2 then statements2 else statements3 fi While statement: while do od; 'while' statements are standard, except that the condition can be a sequence of statements followed by the condition expression. This allows center and tail-exit loops to be built from the same construct. e.g.: while i ~= 10 do cprint(me(), itos(i) + " "); i := i + 1; od; while th := getAThingValue(); th ~= nil do processTheThing(th); od; Proc call: proc-expression([par1 {, parI}]) The correctness of a given procedure is not checked until it is first called. Syntactically, a procedure, represented as characters inside a pair of back-quotes, consists of a parenthesized list of parameter names followed by the body of the procedure. The parameters are not typed and hence their type can vary from call to call, although this is not recommended. If a procedure is to return a result, then put that result after the last statement of the procedure's body. There is no 'return' statement. e.g.: sum := `(a, b) a + b` show := `(th) cprint(me(), "The thing you passed contains:\n"); describe(th); cprint(me(), "Wasn't that interesting?\n")` ... sum(1, 2) show(me()) - call show on the active player show(location(me())) - call show on the location of the active player th.{stringFunc(3, 4)}.actor(th) - call 'stringFunc' with parameters 3 and 4. It must return a string, which is then looked up as a property name on thing 'th'. That property must exist and be another thing. Property 'actor' is looked up on the second thing, and must yield a function, which is then called with 'th' as parameter. fred.joe.sam("mary", ellen, susan + 2) Thing 'fred' must have a property 'joe' which is another thing. That thing must have property 'sam', which is a proc. That proc is called with 3 parameters - the string "mary", the current value of parameter, local or global variable 'ellen', and the similar value of 'susan' + 2. (Hence 'susan' must be an int variable.) Operators (in order of groups of decreasing precedence): * - integer multiplication / - integer division % - integer remainder + - integer addition + - string concatenation - - integer subtraction =, ~= - integer, string, thing, action equality comparison <, <=, >, >= - integer or string sorting comparison Parentheses are used as normal to alter precedence. Builtins: cprint(who, what) - print string 'what' to client 'who'. 'who' is a thing value, and should be one for an active player. aprint(who, what) - print string 'what' to all clients in the same room as 'who', except for 'who' global() - return a pointer to the global context itos(n) - return string form of integer 'n' length(str) - return length in characters of string 'st' substr(str, pos, len) - return a substring of string 'st' starting at position 'pos' (first is position 0) of length 'len'. The result will just get truncated for out-of-range values. strtoproc(str) - return a proc whose body is just the passed string 'str'. Note: strings are dynamic objects which are allocated and freed as needed. Procs are not allocated/copied/freed when used. Thus, the use of this builtin is likely to result in lost memory in the server. random() - return a random signed integer word() - return the next "word" (whitespace terminated) from the current command-line-tail. When 'PARSE_ACTION' is called, the command-line-tail is set to the entire command that the user entered. The "current position" in the command-line-tail is moved up to after the returned word, so successive calls to 'word' will return successive "words" from the command-line-tail. 'substr' can do this splitting apart, but it would do it slowly. gettail() - return the current command-line-tail. settail(str) - set the command-line-tail to 'str'. me() - return the thing for the current player location(who) - return the current location thing for player 'who'. name(who) - return the name of the passed thing. For players, this will be the player name. Do not confuse this with the 'name' property explicitly used on rooms and objects by the supplied scenario. move(who, where) - move the player whose thing is passed as 'who' to the room whose thing is passed as 'where'. This value is saved in the database, can be returned by 'location', and is used by 'aprint'. Note that for 'location', 'name' and 'move', "player" can be a machine. typeof(th, name) - look up the name given by string 'name' in the context represented by thing 'th'. If the name is not a property in that context, return -1, otherwise return: 0 - property is void (should not be possible) 1 - property is an integer 2 - property is a string 3 - property is a proc 4 - property is a thing This is often used to find out if a given thing has a property. The supplied scenario also uses it to retrieve a string to print or a proc to call when a given action is applied to an object. describe(th) - print to the current client a textual representation of the name and properties of the given thing. quit() - arrange that the current client will terminate when the running code eventually returns to the main server code. thing(str) - create and return a new thing with the given name. It is an error if a thing by that name already exists. If the passed string is empty, then a new, unique name will be constructed. destroy(th) - destroy thing 'th'. NO CHECK IS MADE TO SEE IF SOMETHING IS STILL POINTING AT THE THING. find(str) - find and return the thing with the given name. If none is found, return nil. findPlayer(str, loc) - find and return the thing for the player with name 'str' who is in location 'loc'. If 'loc' is nil, then any location is acceptable. If no such player exists or is not currently active, then return nil. forEachPlayer(pr) - the given proc is called once for each active player in the world. The proc is passed that player as its parameter. 'me()' can still be used to find the current player. createMachine(name) - create a new machine with the given name. The context (thing) for the new machine will be returned. If this call is used to create a new machine dynamically, it is likely that 'force' would be used to start the machine operating. Machines can also be created simply by defining them in the initial database given to the server. See the section above on Machines. destroyMachine(thing) - destroy the indicated machine. Dynamically creating and destroying machines would be done, for example, to handle randomly created monsters in a scenario with fighting. after(time, proc) - after roughly 'time' seconds have elapsed, the proc is called for the client or machine who is calling 'after'. This is often used at the end of the normal step action for a machine, in order to trigger the next call of the same action. force(who, proc) - force player or machine 'who' to call the given proc, with thing 'who' as parameter. The call is done immediately. This can be used for a variety of purposes, some useful, others merely annoying. Online Building Online building is possible, but no commands have been provided in the scenario to make it easier. This could certainly be done. Building can be done by directly using the programming language. E.g., using the conventions of the supplied scenario, one could enter: .FredRoom := thing("r_fredRoom1") .FredRoom.name := "in Fred's first room" .FredRoom.desc := "This room is magnificent! ... " .FredThing := thing("o_fredVase") .FredThing.name := "vase" .FredThing.adjectives := "pretty china" .FredThing.desc := "The china vase is very pretty. ... " .FredThing.drop := FredBreakableRoutine .FredThing.next := nil .FredRoom.contents := FredThing .FredRoom.south := location(me()) .location(me()).north := FredRoom go north Note that it is a good idea to not link to the new room until it is fully setup, so that no-one can wander into it prematurely (and get errors from the code looking for properties that aren't there yet!) The global variables used here (FredRoom and FredThing) can be reused when the room and object are finished. These assignments, which are convenient, can easily be re-established later, e.g. by using '.FredRoom := location(me())' or '.FredThing := location(me()).contents'. Database Format The database format is a simple, text-only representation of the world. The format is human-readable, so it is quite possible to build a scenario by simply editing a copy of the initial database and trying it out, much like the edit-compile-link-run cycle of compiler-based programming. On-line building can be done also, and the newly built stuff will be saved to the database file when the server exits, but the database writing routines do not insert blank lines, comments, etc. Also, when creating by editing the database, the builder will likely choose meaningful names for the various things in the scenario, whereas with on-line building they may have only numeric names generated by the system. Thus, it may be wise to do the initial development of a scenario using an edit/test cycle. Blanks lines in the database are skipped when reading it. Lines starting with a '#' are similarly skipped as comments. The first significant line of the file contains the decimal number of the next available numeric value for a thing name. For an empty database, 100 is a suitable value. When building a large scenario, however, you may want to up the value to 1000 or even 10000. When the system writes out the database file, the value used is that of the next free unnamed-thing name. Next in the database come the things in the scenario. Each thing is terminated by a line containing just a single period, and the set of all things is terminated by a line containing just a pair of periods (..). Each thing entry starts out with a line containing the name of the thing. This name must be unique within the entire database. Following the name are lines describing the properties of the thing (variables in its context). Each starts with the name of the property and continues with the value of the property. The property may actually start on the next line - this is useful for string properties, so that newlines end up in the right places. The various property types are represented as follows: integer value - a decimal number, perhaps with leading minus sign proc value - a back-quote (`), followed by the text of the proc (which may span several lines), followed by a closing back-quote. string value - a quoted string, which may span several lines. Each newline in the string is kept as a real newline. Also, backslash escapes for \n, and \" are accepted. thing pointer - the value is simply the name of another thing. That thing must already have been defined. As a special case, the property name for a thing property can be a pair of names with a slash (/) separating them. In that case the first name in the pair is entered in the current thing as pointing to the thing named by the property value, and the second name is entered into the thing named by the property value as pointing to the current thing. This is how the lack of forward references is handled. Note that the database writing code in the server may write the things in the database in a different order than they were read in. Some effort is made to prevent this, however. After the things in the database comes the global context. It is exactly like a single thing, except that it does not have a thing name. It will usually have many more properties (variables) than true things do. After the global context comes the player values. Each player value consists of a line containing the player's name, a line containing the player's password (not encrypted in any way!), a line containing the name of the thing which is the player's current location and then the context for the player, again just like a thing but without the thing name. The set of players is terminated by a line containing a double dot (..). After the players come any machines in the scenario. These appear exactly the same as players, but they will almost always have a 'MACHINE_RESTART_ACTION' proc property, and often a 'LISTEN_ACTION' one as well. The password for a machine is irrelevant but must be present. The Provided Scenario The reader is encouraged to examine the provided scenario - it is not very complicated. Some "standards" within that scenario: thing 'THING_PROCS' is used to hold all of the utility routines in the scenario. This is abbreviated 'P', so that utility routines are called as in 'P.formatName(...)'. For a larger scenario, more similar things could be used in order to modularize the code and perhaps speed up run-time searches. thing 'THING_COMMANDS' is used to store all of the commands in the scenario. Each is stored under the command name or abbreviation, and its value is a parmeterless routine to execute to do the command. This thing is abbreviated as 'C', although the only references to it are from 'PARSE_ACTION'. when 'PARSE_ACTION' is trying to do a command, it checks for the command verb as a property of the player and of the room that the player is in. If found, the property is either a string to print (with added newline) as the entire effect of the command, or is a proc to call to do the command. This allows properties on players and in rooms to override the normal interpretation of commands, and also allows special commands to be attached to players and rooms that are not otherwise available. If the command is not found on the player or the room, then 'PARSE_ACTION' will call P.getNounPhrase to get an object for the command. If that object is either being carried or in the current room, then the command is similarly checked for on the object. Only if all of these checks fail is the command looked up in the global command table, where it must be a proc to call. The command line tail is put back before calling such a routine. A command found on an object is passed the object itself as a parameter. This allows the use of generic actions on several objects. global variables 'true' and 'false' are provided - it is suggested that you use them for all boolean values. objects are linked in lists (room contents, player carrying) through explicit 'next' properties that the scenario must maintain properly objects have a 'name' property, which is the single noun of their name objects may have an 'invisible' property, in which case they will not normally show up in a room contents or player carrying list objects may have an 'adjectives' property, which is a blank-separated list of the relevant adjectives for the thing objects may have a 'desc' property, which can be either a string describing the object, or a routine which is passed the object as parameter and which returns a string describing the object. Both such strings should include proper punctuation, but should not include a terminating newline. objects may have a 'get' property, which can be either a string to be printed if someone tries to pick up the object (the pickup fails) or a proc which is called to see if the pickup succeeds. The proc will be called with the object as parameter (so generic 'get' routines can be used), and the get will only succeed if the proc returns a nonzero value (e.g. 'true'). objects may have a 'drop' routine, which is handled exactly analagous to a 'get' property. rooms have a 'name' property which is a string which makes sense to print in the format "You are " + room.name + ".\n" rooms have a 'contents' property, which heads a list of the objects in the room rooms may have a 'desc' property for the long description of the room. It can be a simple string, which will be printed along with a final newline, or a proc which will be called to return a string which is the description of the room. rooms are linked with properties north, south, east, west, northeast, northwest, southeast, southwest, up, down, in and out. players have a 'carrying' property which is a list of the objects they are carrying players have a 'verbose' flag, which is either 1 or 0, controlling whether or not they get the full description when they walk into a room. players may have a 'desc' property, which can be either a simple string which will be printed with a newline when someone looks at them, or it can be a proc which should yield a string which will be printed with a trailing newline. Some utility routines provided in the scenario: formatName(obj) - passed an object, returns a properly formatted name of that object, containing the object's name and any adjectives showObjects(desc, list) - if the list is empty or none of the objects in it are visible, do nothing and return 'true', otherwise print 'desc' and then use formatName to print the name of each visible object in the list, indented by two spaces, and return 'false'. playerIsHere(who) - used only as part of 'showPlayersAndStuff' showPlayersAndStuff() - print a one-line message saying someone is here for each player in the same room as the current player, and also use 'showObjects' to print the objects in the player's location. showHere - print a long description of the player's location lookAround - print either a long or a short description of what the player can see, depending on the player's 'verbose' flag showOneObject(object) - print a long description of the object showPlayer(player) - print a long description of the player tryMove(dir) - attempt to move in the given direction. 'dir' is a string which is a standard direction name. The property is looked up in the current location. Any result must either be a thing which is the room in that direction, a string to print to explain why going in that direction doesn't work, or a proc to call to do the entirety of going in that direction. getNounPhrase() - pull a noun phrase out of the current contents of the tail buffer. The returned string will have the noun (last word in the tail buffer) first, followed by any adjectives given. There will be a single space between the words, and one at the end. showName(objname) - takes an adjective/noun set as returned by 'getNounPhrase' and returns it back in the normal order, with no extra spaces. matchName(th, pr, objname) - takes a thing, the name of a property to look up on that thing, and a string in the form as returned by getNounPhrase. Returns 'true' if the value of the property exists, is a string, and one of the words in it (it is a series of blank-separated words) is the same as the first word in the passed 'objname' (i.e. the noun). This routine modifies the tail buffer during its operation. findObject(list, objname) - look through 'list' for an object whose name and any adjectives match the noun/adjectives passed as 'objname' (in the format as returned by 'getNounPhrase'). The first matching object is returned, and 'nil' is returned if none is found. This routine modifies the tail buffer during its operation. deleteObject(list, object) - if the object is in the list, then it is deleted from that list. The head of the resulting list is returned (which is needed since the object could be at the head of the list and all parameters are strictly call-by-value). findPlayerHere(name) - name is in the 'getNounPhrase' format. If there is a player by that name in the same location as the current player, then return that player, otherwise return nil. genericVerb(verbString, propName) - this utility can be used to easily implement additional simple verbs. A call to it can be used as the entire body for a new command in 'THING_COMMANDS'. It is passed the command that was used by the user (or a normalized form of it), and the name of a property to look up on objects. It will parse off a noun phrase, and attempt to find some object matching that in the current room or in the player's inventory. If such an object is found, then it will check for the property on that object. If found, the property must be either a string to print (with trailing newline added) or a proc to call with the object as parameter, as the entire action of applying the command to the object. If the object has no such property then a message saying that you can't apply the verb to the object is printed. If no matching object is found, a message indicating so is printed. Things To Do This section describes some of the many weakness of ToyMUD, and suggests some things that you may want to do about them. Note that I do NOT plan on doing any of these things or on being a coordinator for changes and upgrades. My interest in ToyMUD has mainly been in how small it can be and still do what I need it to do. Add boolean 'and', 'or' and 'not' This shouldn't take more than an hour. Add the tokens to TokenType_t. Add the words to ReservedWords. Add a couple of routines to the recursive descent parser/interpreter between 'parseComparison' and 'parseExpression', and make 'parseExpression' call the top new one. [See! I set it up just so you could easily do this.] Invisibility Add the concept of invisible players to the scenario. This should be easy - just fix P.playerIsHere and P.showPlayersAndStuff, add commands to set/unset the flag on the current player, and modify 'say' and 'pose' appropriately. Darkness Add the concept of darkness to the world. This would involve adding a flag to rooms that are to be dark, perhaps adding a flag to objects and/or players that give off light, modifying P.showHere to take the flags into account, and modifying several of the user commands to perhaps fail in the dark, but at least not provide visual indication of things being done. More multiplayer stuff Actions that a player can do should be seen by others in that location. This would include actions like getting and dropping things, operating things, etc. Examine the code and add appropriate messages. Pay attention to player invisibility and darkness if you have added them.