Lets make a mud engine.. but first a scripting language.

posted in Jemgine
Published September 14, 2011
Advertisement
Lets make a mud! Why? Why not? I love muds, which is very odd because one thing I do not love is actually playing muds. They are sort of a poor-man's MMORPG, an MMO you actually can run out of your living room. It amazes me that there aren't more people making them. I've actually built a mud engine from scratch before. Four times, in fact, which will make this the fifth. This time around, I'm going to do it in the style of an LPMud. In an LPMud, each object in the game world is a 'class' in a script file. The LPMud is really a shared terminal to a compiler and interpreter. The game is a library written in the LPMud language. This won't be quite so primitive, but it will need a scripting language of some kind. So, at the very least, this is an excuse to write a scripting language, and I might as well start there.

So first I create some projects, and I download Irony from codeplex - http://irony.codeplex.com/ . Then I start defining the grammar in Irony.


var numberLiteral = TerminalFactory.CreateCSharpNumber("number");
numberLiteral.AstNodeType = typeof(IntLiteralNode);

var expression = new NonTerminal("Expression");
var parenExpression = new NonTerminal("Paren Expression");
var binaryOperation = new NonTerminal("Binary Operation", typeof(BinaryOperationNode));
var _operator = ToTerm("+") | "-" | "*" | "/";

expression.Rule = binaryOperation | numberLiteral | parenExpression;
binaryOperation.Rule = expression + _operator + expression;
parenExpression.Rule = ToTerm("(") + expression + ")";


I start with the most basic constructs in the language. This can parse basic math expressions using the +, -, * and / operators and integer literals. There's a little more Irony needs to parse it correctly that I won't go over here. When Irony parses my script, it creates an Abstract Syntax Tree. The tree is made of BinaryOperationNodes and IntLiteralNodes. Each of these is an expression node.


public class ExpressionNode : AstNode
{
public virtual void compile(List bytecode, ExecutionContext context) { throw new NotImplementedException(); }
internal virtual String getResultType(ExecutionContext context) { throw new NotImplementedException(); }
}


At this point, I could lay out a set of operation codes that includes things like 'add' and 'mul', but instead I am going to implement a 'system_call' operation code. I'll need some way to register 'system functions'.


public class ExecutionContext
{
internal Dictionary systemFunctions = new Dictionary();
internal List systemFunctionTable = new List();

internal String decorateFunctionName(String name, params String[] parameterTypes)
{
return name + String.Join("@", parameterTypes);
}

public void addSystemFunction(
String name,
String returnType,
Func implementation,
params String[] parameterTypes)
{
var systemFunction = new SystemFunction();
systemFunction.name = name;
systemFunction.returnType = returnType;
systemFunction.parameterTypes = parameterTypes;
systemFunction.systemFunc = implementation;
systemFunctions.Add(decorateFunctionName(name, parameterTypes), systemFunction);

systemFunction.funcTableId = systemFunctionTable.Count;
systemFunctionTable.Add(systemFunction);
}
}


The 'system functions' take an array of objects and return an object. They are terribly generic. My language's stack is a stack of objects. This makes implementing the 'systemcall' opcode fairly straightforward.


case Instruction.systemcall:
{
var _funcID = BitConverter.ToInt32(bytecode.GetRange(instructionPointer + 1, 4).ToArray(), 0);
var _func = context.systemFunctionTable[_funcID];
var parameterArray = new object[_func.parameterTypes.Length];
for (int i = 0; i < parameterArray.Length; ++i)
parameterArray = stack.fetch(parameterArray.Length - i);
for (int i = 0; i < parameterArray.Length; ++i)
stack.pop();
stack.push(_func.systemFunc(parameterArray));
instructionPointer += 5;
}
break;


It looks up the function, then it copies the arguments out of the stack, pops the arguments off the stack, and pushes the result. The index '1' always points to the top of the stack, '2' points to the next thing down, and so forth, and it assumes the arguments were pushed in order, so the first argument is at 'argument count' and the next one is at 'argument count - 1'. The BinaryOperationNode will use this opcode to implement itself.


public override void compile(List bytecode, ExecutionContext context)
{
var func = context.lookupFunction(AsString, (ChildNodes[0] as ExpressionNode).getResultType(context),
(ChildNodes[1] as ExpressionNode).getResultType(context));
if (func == null) throw new InvalidOperationException("Operator not defined");
(ChildNodes[0] as ExpressionNode).compile(bytecode, context);
(ChildNodes[1] as ExpressionNode).compile(bytecode, context);
bytecode.Add((byte)Instruction.systemcall);
bytecode.AddRange(BitConverter.GetBytes(func.funcTableId));
}


First it makes sure the operation actually exists. Then it compiles each expression. It assumes these expression leave their result on the stack. So after two run, there should be two results on the stack. I do need to report errors better.

So I can parse simple expressions, and compile them into bytecode, and execute that bytecode. So the last thing to do is test it. First I setup a context and define some operators.


var context = new DeyjaScript.ExecutionContext();

context.addSystemFunction("*", "int", (parms) => { return (parms[0] as int?).Value * (parms[1] as int?).Value; }, "int", "int");
context.addSystemFunction("/", "int", (parms) => { return (parms[0] as int?).Value / (parms[1] as int?).Value; }, "int", "int");
context.addSystemFunction("-", "int", (parms) => { return (parms[0] as int?).Value - (parms[1] as int?).Value; }, "int", "int");
context.addSystemFunction("+", "int", (parms) => { return (parms[0] as int?).Value + (parms[1] as int?).Value; }, "int", "int");


This is a terrible way to implement basic integer math operators but they work. Did I mention this is terrible? Next I parse the script and compile it.


var grammar = new DeyjaScript.Grammar();
var parser = new Irony.Parsing.Parser(grammar);
var program = parser.Parse("(4 * 4) / (8 - 6) + 6 - 3 * 4");
var rootNode = program.Root.AstNode as DeyjaScript.ExpressionNode;

var bytecode = new List();
rootNode.compile(bytecode, context);


And finally I can execute it. There should be a single thing left on the stack, the result of the expression.


var VirtualMachine = new DeyjaScript.VirtualMachine();
VirtualMachine.execute(bytecode, context);
Console.WriteLine("Result : " + VirtualMachine.getStackTop().ToString());


The result? 4!

Wait.

So I'll stick the source code so far on this somehow. And next time I'll expand on the language a bit with, I don't know; lets say variable declarations. It's hard to add just one thing at this point, the language sort of needs everything at once to do anything interesting.
1 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement