Zork/MUD-like basic item structure
So at work the other day I was explaining Lantern to a buddy (@shamuspeveril) and we somehow got onto the topic of how well nodejs would work for a zork like game or a mud. Well, sure enough, with the idea stuck in my head I sat down today and tried to think through. Luckily I think this concept will apply well to what I hope to do with the Lantern server, so I don't feel like I deviated too much from my path.
In case you are not sure what a MUD is, this is a brief description grabbed from wiki :
MUDs combine elements of role-playing games, hack and slash, interactive fiction, and online chat. Players can read or view depictions of rooms, objects, other players, non-player characters, and actions performed in the virtual world. Players typically interact with each other and the world by typing commands that resemble a natural language.
And if you don't know what Zork is... Leave. Just go.
Anyways, making the basic nodejs server to handle limited verbs was pretty simple. Ex:
Server:
[code]
var http = require("http");
var url = require("url");
var fs = require("fs");
var sys = require("sys");
var qs = require("querystring");
var send404 = function(res){
res.writeHead(404);
res.write('404');
res.end();
}
var server = http.createServer(function(req,res)
{
var path = url.parse(req.url).pathname;
sys.puts(path);
switch (path)
{
case "/":
fs.readFile('mud/index.html', 'utf8', function(err, data)
{
if(err) {
res.writeHead(500, {});
res.end();
throw err;
}
res.writeHead(200, {
'Content-Length': data.length
, 'Content-Type': 'text/html'
});
res.write(data);
res.end();
});
break;
case "/command":
var commands = qs.parse(url.parse(req.url).query).commands;
handleCommands(commands);
break;
default:
if (/\.(js|html|swf)$/.test(path)){
try {
var swf = path.substr(-4) == '.swf';
res.writeHead(200, {'Content-Type': swf ? 'application/x-shockwave-flash' : ('text/' + (path.substr(-3) == '.js' ? 'javascript' : 'html'))});
res.write(fs.readFileSync(__dirname + path, swf ? 'binary' : 'utf8'), swf ? 'binary' : 'utf8');
res.end();
} catch(e){
send404(res);
}
break;
}
send404(res);
break;
}
});
server.listen(8000);
function handleCommands(commands)
{
var commandArr = commands.split(" ");
verbs(commandArr[0]);
}
function verbs(v)
{
switch(v)
{
case "n":
case "north":
sys.puts("Moving north");
break;
case "s":
case "south":
sys.puts("Moving south");
break;
}
}
[/code]
Client :
[code]
$(function(){
var commandObj = {
commands: document.getElementById('commands').value
}
$.get("http://localhost:8000/command", commandObj,
function(data)
{
}
, "json");
});
[/code]
So that just handles north/n and south/s but you can see how it would progress. Which should lead you to the problem I hit when I added the "south" command block - That is going to be one massive switch statement. So instead of continuing forward with my massive switch I decided to think about it yesterday and I think I came up with a solution that I will use also in Lantern for my crafting system.
The system relies on the item know what is can do and containing all the information that would make it able to control itself instead of a large switch/case on my part. And this whole set up is made 100 times easier because I'm going to be using mongodb. Here's some pseudo code to give you an idea of what I'm talking about:
[code]
{
type:"Room",
exits:
{
north:{desc:"You see an exit to the north",room:3242},
south:{desc:"There's a small door that seems to be locked",room:123}
},
description: "You're in a big room with two doors.",
items:"123,63,7456,34,99,4563,215789,45"
commands:
{
look:{desc:"The room you are in is stupidly large with only two tiny doors, one to the north and a seemingly locked one to the south."};
yell:{desc:"You shout at the top of your lungs and you just hear your voice echoing mockingly off the walls."}
}
}
{
type:"Item",
id:123,
description: "The rock is small and perfectly circular.",
commands:
{
take:{desc:"You take the rock and put it in your backpack.",callback:"addToInventory"};
}
}
{
type:"Item",
id:99,
description: "A small glass bottle filled with an unknown liquid."
commands:
{
combine:{desc:"Blah blah", callback:combineWith, ingredients:4345, creates:8456}
}
}
[/code]
As you can see with a system like this, each item is responsible for knowing everything about it and the other items it interacts with. While this make be more difficult to create up front, I feel that once the system is in place I could easily use this in Lantern, a MUD or really any game going forward.
Or maybe I'm brutally over thinking the problem. Is there a better way to handle item relations and verbs? Would a massive switch/case with all the different verbs I want be the way to go? Even if I got that path, I need a similar structure to above just for the content. And then added a simple onetime use command like "lick" or something would require new code instead of just text.
Let me know what you think. I will be starting on code like this soonish for Lantern and would like to have a method pinned out. Right after I figure out the mapping issues I'm having ^^
Related posts:
June 6th, 2010 - 05:57
You might want to consider treating the player as an object as well, and adding some commands to that instead of having them on objects, for example.
Specifically I’m thinking of manipulation actions like “take” or “drop” — once the rock is in their inventory, you’ll probably want them to continue being able to examine it, but clearly you won’t want them to “take” it.
Of course, figuring out where exactly all of the actions should be attached will probably be the trickiest part of all of this. Good luck.
June 6th, 2010 - 06:50
Ya, the character is also going to be an object, but I didn’t really think of putting the functions like drop/take on the player. That would likely make things much easier eh?
I would also leave such commands on the items as well I think. Normally if I ‘take’ an item, the character object will try to add it to it’s inventory, but first it will check if the item as well has a ‘take’ command. So if an item is trapped and the trigger is to ‘take’ it, the item is aware of what it’s going to do.
Yes, that sounds like a much better approach.
What do you think about combine? I think that should be on the player as well. Then it will easily be able to each their players skills or stats or status before combining, which it will then check the items themselves for combine commands to see what is required and the outcome will be.
June 6th, 2010 - 07:31
A lot of MU* systems that I’ve encountered have the idea of actions on the one side and a permission system on the other — basically, my player object is the one that deals with moving things to and from my inventory, while the object stores who is allowed to do what to it. Especially in a MUSH or MOO environment, where people are creating and coding objects, it’s useful to have a concept of the “owner” of an object, and the ability to set permissions based on roles (usually just owner vs other, but you could put in “trusted” players, guild members, etc etc).
Of course, all of this is potentially a little excessive for what you’re currently doing. Maybe? I don’t know how complex Lantern’s crafting system will be.
(P.S. If you had an online game where people could craft not just out of a recipe list, but create entirely new items, that would be pretty hardcore. To have powerful, player created-and-named-and-described artifacts could go a long way towards investing players in the world.
Or at least, having “extras” that you could toss in — if I add Crushed Ruby to my Broadsword then I get a Broadsword of Flame + 1 or whatever.
Just saying.)
June 6th, 2010 - 08:54
Having gone the route of adding the commands to the player object, I can tell you it makes for a very FAT object. Lots and lots of logic on the player that really doesn’t belong there.
My refactoring of this has led me to creating “Service” objects who handle the responsibility of actions. A movement service, a combining service, a chat service, etc. Leaving the Player and Item objects mostly responsible for storing/retrieving themselves along with validations and such.
Here’s a post that explains the approach in more detail: http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html
June 6th, 2010 - 17:17
Hey Shamus,
I will tell you more about it tomorrow.
You are pretty close to how I plan my system to work
Hey Ben, I can get the idea of a service, but I can’t see a way to make the objects smaller other than taking out the callbacks and having a large switch. Maybe I’m missing something when you talk about a service class, but from my understanding I would just pass the object (everything but it’s callback) to a class that takes my command and the object and tries to figure it out instead of calling the callback? I’m not saying the method I outlined is an ideal method (far from it) but I don’t seem to see how a ‘service’ method is better.
June 7th, 2010 - 10:34
The switch statement for me was replaced with an Eval. Using the verb of the action, I can tell Ruby to instantiate a class based on that name (possibly adding something to it, like “Service”).
My server receives a valid command to “move.” It then tries to instantiate a “MoveService” Class. They may succeed, or fail. If it fails, I don’t have a class for that action yet (and could default to a “IDontKnowHowToDoThatService”). If it succeeds, then I just tell the MoveService to execute().
The benefit is well defined responsibilities of all objects — so when you go looking for a problem, it’s easier to see where to fix it. Or, conversely, you have nice bottlenecks to incorporate special cases. e.g. A movement service that’s based on the type of player (“giant”,”human”,”werewolf”,etc.). In those cases you incorporate the player’s class into the action class name (VampireMovementService).
I don’t know PHP well enough to say whether this approach might work for this case or not.
June 8th, 2010 - 05:18
That is a pretty interesting approach. I will have to mull this over and see what I can do with a few examples. I’m currently not using PHP though. Everything is in JS.