ID:2274216
 
Hey guys,

Just wanted to stop by Off Topic and introduce myself. I've been programming since I was about 8, thanks to an old Commodore 64 my dad found in the garbage. Learned BASIC growing up. Discovered BYOND in 2000 or a little before (I still remember the DUNG website and logo, and the old black and blue forum interface). After BYOND, I learned PHP, JavaScript, VisualBasic, C/C++, and a bunch of other techie things, like building Arduinos and Raspberry PIs to automate processes. Actually built a keg washer powered by an Arduino and glycol fermentation controller with an Arduino + Raspberry Pi.

For the past few years, I've been running my own small business. I had a nano-brewery in a small town. Recently, we were forced to close due to lack of cash-flow. There just wasn't enough craft beer drinkers in town. It's no big deal to me, really. Sometimes you try to offer a community something, and it's not really what they wanted. You can't really fault anybody; you just have to pick up the pieces and move on.

Having extra time on my hands, I decided to revisit BYOND. I had attempted to program a scripting language inside of BYOND a number of times, referring to some books I picked up on Amazon for guidance. I never succeeded until this past time. Not sure why, but suddenly it all "clicked." I am having a lot of fun getting back into programming with BYOND. I had forgotten how beautiful of a language it is.

It saddens me that I only recognize a few names on the forum, but it is good to see that the community is very much still active and that Lummox has added a TON of new features.

I have 2 games on the drawing board, but only 1 in active development. I'm working on a text-based MUD. Might seem crazy, but I think the genre needs a major kick in the butt. I've been playing a few of them, and I am amazed that the genre hasn't progressed at all since the 90s. It occurred to me that it wouldn't take much to make a MUD that is significantly better than the ones currently out there.

The other game relies heavily on the scripting language I developed, so I'm going to wait until I have that dialed in a little better. For now, I'm using the scripting language in the MUD I'm working on to help refine it.

Thanks for reading and I'm looking forward to being a more active member of the community.
Welcome back and good luck on your endeavors! (^8
Always happy to see someone working on a MUD :) Welcome back.
Scripting language?!

Also the interface to it so far looks stunning.

[edit] Your scripting language looks amazing, I'd love to see how you pulled it off.
In response to Kozuma3
Kozuma3 wrote:
Scripting language?!

Also the interface to it so far looks stunning.

[edit] Your scripting language looks amazing, I'd love to see how you pulled it off.

I'd like to polish it up and do a nice write-up. Part of what made it successful is that BYOND now supports Regex. So the lexer is basically just a series of regex rules. It breaks up the code into tokens specified by regex. Then it goes to a parser, which expects certain sequences of tokens and creates what's called an abstract syntax tree or AST for short. The AST is just a bunch of datums with child datums, like a tree. Then, the parent datum's compile function starts and calls the compile function of the children such that in generates a post-fix series of instructions. It's not technically byte code, since the output of the compiler is actually a BYOND list of instructions, but it functions the same way for the most part. An interpreter datum then executes the instructions in the list. I'll post some examples later.
Sounds like the successful thinker, I might follow your footsteps. TO HELL AND BACK!!!
In response to Gambrinus
Gambrinus wrote:
...

I've googled AST and almost everything related to parsing, just can't seem to get myself to want to try regex as I prefer manually parsing everything. The datum with datum's connected to form a tree is something I've been wanting to try, does the syntax support tabs and such?

One issue I've had myself is trying to capture things such as
func Main()
{ //A
;
func Demo()
{ // C
;
} // D
} //B


Finding the ending of A(B) while another function exists farther down. The syntax of your language looks beautiful. Would love to tinker around with it when it's ready :)

This is the first scripting language I made, it sure as hell isn't pretty to look at but it did the job :)
http://www.byond.com/ forum/?post=2050051&page=2#comment19376250
Regex is certainly not required. My initial attempts just went character by character using a series of functions to recognize different tokens. The advantage of using Regex is that the code is easier to read and expand upon.

This needs to be cleaned up a little bit yet, but here is what my lexer looks like:

Lexer
var
list/lex_rules = list()
list/ignore_expressions = list()
buffer
buffer_len
pos

proc/Ignore(exp)
ignore_expressions += exp
proc/Add(name, exp)
lex_rules += new/LexRule (name, exp)

proc/Tokenize (text)
buffer = text
pos = 1
var/line_num = 1
var/last_line_term = 1
buffer_len = length (buffer)
var/list/tokens = list()
var/regex/line_term = regex("\n")
while (pos <= buffer_len)
var/start_pos = pos
var/LexToken/new_token
for (var/regex/expression in ignore_expressions)
if (expression.Find(text,pos)==pos)
pos += length (expression.match)
if (pos == start_pos)
for (var/LexRule/rule in lex_rules)
if (rule.expression.Find(buffer, pos)==pos)
pos += length(rule.expression.match)
new_token = new (rule.name, rule.expression.match)
break

if (new_token)
new_token.line_num = line_num
new_token.pos_num = start_pos
tokens += new_token

if (pos == start_pos)

world.log << "error (line: [line_num], pos: [pos-last_line_term]): Unrecognized token... [copytext(text,pos,pos+6)]"
break

// Count line numbers
while(line_term.Find(buffer, last_line_term, pos))
line_num ++
last_line_term = line_term.index+1

return tokens



LexRule
var
name
regex/expression
New (n, r)
name = n
expression = r
return ..()

LexToken
var
name
value

// debugging info
line_num
pos_num
New(n, v)
name = n
value = v
return ..()

Lexer/DWAD
New()
. = ..()
// Reserved words
Add("LIST", regex("list","i"))
Add("IF", regex("if","i"))
Add("SWITCH", regex("switch","i"))
Add("CASE", regex("case","i"))
Add("DEFAULT", regex("default","i"))
Add("ELSE", regex("else","i"))
Add("FOR", regex("for","i"))
Add("WHILE", regex("while","i"))
Add("BREAK", regex("break","i"))
Add("PRINT", regex("print","i"))
Add("INPUT", regex("input","i"))
Add("INT", regex("int","i"))
Add("STR", regex("str","i"))
Add("DEF", regex("def","i"))
Add("TO", regex("to","i"))
Add("IN", regex("in(?:\[^a-zA-Z_])","i"))
Add("RETURN", regex("return","i"))

Add("NUMBER", regex("\[0-9]*\\.?\[0-9]+"))
Add("STRING", regex("\".*?\""))
Add("WORD", regex("\[a-zA-Z_]\[a-zA-Z0-9_]*"))

Add("INCREMENT", regex("\\+\\+"))
Add("DECREMENT", regex("\\-\\-"))
Add("AEQUALS", regex("\\+="))
Add("SEQUALS", regex("\\-="))
Add("MEQUALS", regex("\\*="))
Add("DEQUALS", regex("\\/="))
Add("PEQUALS", regex("\\^="))

Add("ADDOP", regex("\\+"))
Add("MINUSOP", regex("\\-"))
Add("DIVOP", regex("\\/"))
Add("MULTOP", regex("\\*"))
Add("LPAREN", regex("\\("))
Add("RPAREN", regex("\\)"))
Add("POWEROP", regex("\\^"))
Add("LBRACE", regex("\\{"))
Add("RBRACE", regex("\\}"))
Add("LBRACKET", regex("\\\["))
Add("RBRACKET", regex("]"))

// Conditionals
Add("ETET", regex("==="))
Add("NEVONET", regex("!=="))
Add("ETO", regex("=="))
Add("NETO", regex("!="))
Add("GTETO", regex(">="))
Add("LTETO", regex("<="))
Add("GT", regex(">"))

Add("LT", regex("<"))
Add("NOT", regex("!"))

// logical operators
Add("AND", regex("&&"))
Add("OR", regex("\\|\\|"))

// assignment
Add("EQUALS", regex("="))



Add("NEWLINE", regex(";"))
Add("COMMA", regex(","))

Ignore(regex("\\/\\*\[\\d\\D\n]*?\\*\\/","m"))
Ignore(regex("\\/\\/.*?\n"))
Ignore(regex("\[\\s\n]+"))


Once you have a list of tokens, you can begin parsing the tokens into useful information (ASTs).

Alex Gaynor gives a great overview of how scripting languages and interpreters work here:
https://www.youtube.com/watch?v=LCslqgM48D4
That looks absolutely amazing, I LOVE the way you have it laid out and the way you add rules is genius and simplistic.

My biggest issues are branching and finding the start & end of things such as procs without using something like end func or end proc. I've started on a scripting language that's going to check for tabs in the beginning to use as a way to solve the above issues.

That is truly great in my eyes tho, I love scripting languages qwq
In response to Gambrinus
Gambrinus wrote:
https://www.youtube.com/watch?v=LCslqgM48D4

Yea, I'm to stupid for that lol.

Your code is much easier to understand the process of converting data to the needed form to run through, executing/reading through tokens is easy tho imo.

I wonder how fast/effecient it is, would calling it frequently cause Add()'s proc overhead to effect it much?

example to improve performance.
Lexer/DWAD
New()
. = ..()
lex_rules = list(
new/LexRule("LIST",regex("list","i")),
//...
)


I've always wondered about a scripting language built to build scripting languages, allowing for the easy parsing/tokenization/execution of things and a way to easily build a syntax.
Hey Kozuma3,

Yeah, there are probably plenty of performance tweaks you could make. However, since the only performance I'm concerned with at the moment is in the interpreter (the part that runs at run-time), I'm not too worried. I've used it so far to compile pretty big programs and haven't had any issues. Once the program is compiled down to bytecode, it doesn't need to be recompiled unless you change something. What this means for games is that you might have a program stored in a datum that can be used over and over again without revisiting the lexer or compiler. In short, the only performance that really matters is the interpreter. The compiler's performance only becomes important when you run into issues compiling large amounts of data.

The biggest performance increase you can make is to add an internal method for keeping track of changes to source code and only compiling when necessary. Currently, my game compiles all scripts upon world.New(). However, I could really store these compiled programs and just load them on world.New(), only recompiling when necessary.

I guess it's one of those issues where if you worry about optimizing before there's an issue, you kind of gridlock yourself from further development. I can think of plenty of ways to optimize the engine in the future. That's why I haven't released it yet ;-).

[edit]Also, I agree. Tokenizing is the easy part. That part probably only took me an hour or two to write. The parser, on the other hand, took me a few days, and when something breaks or isn't working right, it's probably the parser. Compiling is simple, because it's basically just taking syntax trees and outputting post fix instructions.

For instance, in my parser, an expression is defined as [term] + or - [term].
Once an expression datum is created, it has two child term datums and an operator.
The compiler compiles the children first, and then adds the operator as an interpreter command.
So 2+2 becomes
PUSH 2, PUSH 2, ADDOP
ADDOP pops 2 variables from the stack, adds them together, and pushes the sum back to the top.

So as you can see, the interpreter is simple. The lexer is simple. The compiler is pretty much simple. The parser is the messy part. I'll do some write-ups at some point.
Here's an example of my scripting language working in Everheart. I call the language Dream Within a Dream. As an engine, it doesn't do anything. It's just a lexer, parser, compiler, and interpreter. You can build any kind of language you want with it. I will provide some implementation examples, but here's what the language I designed looks like:

Triviabot:
players = list();
scores = list();
game_started = 0;
waiting_for_players = 0;
round = 0;
bored_timer = 0;
do_next_round = 0;
round_start_time=0;
qnum=0;
answer = 0;
questions = list (
"Which is the most abundant metal in the earth's crust?",
"What was first feature length animated film?",
"What is the only sea on Earth with no coastline?",
"In what year is Columbus credited with discovering the new world?");

answers = list (
"aluminum",
"snow white",
"sargasso",
"1492");


def StartGame() {
game_started = 1;
waiting_for_players = 0;
bored_timer = 0;



for (i = 1, i <= len(players), i++) {
scores += 0;
}
say ("Let's get this game started!");
sleep (10);
say ("Listen up, deez are the rules.");
sleep (10);
say ("Each round, I'll ask a question.");
sleep (30);
say ("Ye got 60 seconds to say the answer.");
sleep (30);
say ("First person who answers gets the point and we go to the next round.");
sleep (30);
say ("The game stops when a player gets 5 points or after 20 rounds, whichever comes first.");
sleep (30);
say ("Any questions?");
sleep (10);
say ("Too bad.");
do_next_round = 1;
}

def ResetGame() {
players = list();
scores = list();
game_started = 0;
waiting_for_players = 0;
round = 0;
bored_timer = 0;
do_next_round = 0;
qnum=0;
answer = 0;
round_start_time = 0;
}


def InitiateWin(player) {
say (player + " wins!");
say ("Here be the final scores!");
for (i = 1, i <= len(players), i++) {
say (players[i] + ": " + str(scores[i]));
}
ResetGame();
}

def NextRound() {
round = round + 1;
round_start_time= time();
if (round < 20) {
do_next_round = 0;
say ("Round " + str(round));
qnum = rand(1, len(questions));
say (questions[qnum]);
answer = answers[qnum];
}
else {
say ("Game over! We finished all 20 rounds.");
highest_score = -1;
winner = 0;
for (i = 1, i <= len(players), i++) {
if (scores[i] > highest_score) {
highest_score = scores[i];
winner = players[i];
}
}
InitiateWin (winner);
}
}


def GainPoint(player) {
say ("Aye, that's it "+player);
for (i = 1, i <= len(players), i++) {
if (players[i]== player) {
scores[i] = scores[i] +1;
say ("Your score is now: " + str(scores[i]));
if (scores[i] >=5) {
say ("Attenion all, we have a winner!");
InitiateWin(player);
}
else do_next_round = 1;
}
}
}
def tick() {
if (game_started == 0) {
if (waiting_for_players == 0 && len(players)>=1) {
waiting_for_players = time();
say ("I'll wait 60 seconds for more players.");
bored_timer = time();
}
if (waiting_for_players>0) {
if (time() > bored_timer + 100) {
bored_timer = time();
command (pick(list("tap", "yawn", "dance")));
}
}
if (time() > waiting_for_players + 600 && len(players)>=1) {
StartGame();
}
}
else {


if (do_next_round) NextRound();

if (time() > round_start_time + 600) {
say ("Times up!");
say ("Corrent answer was " + answer);
do_next_round = 1;
}

}


}
def receive(player, msg) {
msg = lowertext(msg);
if (findtext(msg, "trivia")) {
say (player + ", if yer lookin' to play trivia, just say you want to join!");
}

if (findtext(msg, "join")) {
if (game_started == 1) {
say("Sorry, " +player+", but the game has started. Wait for the next round!");
}
else {
players += player;
say("Aye, " + player + ", I gotcha down.");

}
}
if (game_started) {
if (player in players) {
if (findtext(msg, answer)) GainPoint (player);
}
}
}


That code, once lexed, parsed, and compiled, is basically a datum with a couple lists. One list is a list of instructions to run the code. The second list is a list of "registers." Interpreters don't know what variables are in the same way that a parser does. Instead, they keep track of information on a runtime stack and registers. So as the compiler goes through the code, it enters different namespaces and recognizes when it reaches a new variable. When it reaches a new variable, it assigns it a number in the register.

Anyway, if you compile the above code with my compiler, it outputs this list of instructions. Really, these are all numbers. I've used a pretty printer to make it easier to read.
[1] LIST_MAKE0[3] MOVE 1[5] LIST_MAKE0[7] MOVE 2[9] LOAD_CONST 1[11] MOVE 3[13] LOAD_CONST 2[15] MOVE 4[17] LOAD_CONST 3[19] MOVE 5[21] LOAD_CONST 4[23] MOVE 6[25] LOAD_CONST 5[27] MOVE 7[29] LOAD_CONST 6[31] MOVE 8[33] LOAD_CONST 7[35] MOVE 9[37] LOAD_CONST 8[39] MOVE 10[41] LOAD_CONST 9[43] LOAD_CONST 10[45] LOAD_CONST 11[47] LOAD_CONST 12[49] LIST_MAKE4[51] MOVE 11[53] LOAD_CONST 13[55] LOAD_CONST 14[57] LOAD_CONST 15[59] LOAD_CONST 16[61] LIST_MAKE4[63] MOVE 12[65] GOTO 176[67] RETURN[68] LOAD_CONST 17[70] MOVE 3[72] LOAD_CONST 18[74] MOVE 4[76] LOAD_CONST 19[78] MOVE 6[80] LOAD_CONST 20[82] MOVE 13[84] GET 13[86] GET 1[88] INTERNALCALLlen[90] COM_LTETO[91] IFNE 109[93] GET 2[95] LOAD_CONST 21[97] ADD[98] MOVE 2[100] PUSH_LITERAL 1[102] GET 13[104] ADD[105] MOVE 13[107] GOTO 84[109] LOAD_CONST 22[111] INTERNALCALLsay[113] LOAD_CONST 23[115] INTERNALCALLsleep[117] LOAD_CONST 24[119] INTERNALCALLsay[121] LOAD_CONST 25[123] INTERNALCALLsleep[125] LOAD_CONST 26[127] INTERNALCALLsay[129] LOAD_CONST 27[131] INTERNALCALLsleep[133] LOAD_CONST 28[135] INTERNALCALLsay[137] LOAD_CONST 29[139] INTERNALCALLsleep[141] LOAD_CONST 30[143] INTERNALCALLsay[145] LOAD_CONST 31[147] INTERNALCALLsleep[149] LOAD_CONST 32[151] INTERNALCALLsay[153] LOAD_CONST 33[155] INTERNALCALLsleep[157] LOAD_CONST 34[159] INTERNALCALLsay[161] LOAD_CONST 35[163] INTERNALCALLsleep[165] LOAD_CONST 36[167] INTERNALCALLsay[169] LOAD_CONST 37[171] MOVE 7[173] PUSH_LITERAL 0[175] RETURN[176] GOTO 222[178] RETURN[179] LIST_MAKE0[181] MOVE 1[183] LIST_MAKE0[185] MOVE 2[187] LOAD_CONST 38[189] MOVE 3[191] LOAD_CONST 39[193] MOVE 4[195] LOAD_CONST 40[197] MOVE 5[199] LOAD_CONST 41[201] MOVE 6[203] LOAD_CONST 42[205] MOVE 7[207] LOAD_CONST 43[209] MOVE 9[211] LOAD_CONST 44[213] MOVE 10[215] LOAD_CONST 45[217] MOVE 8[219] PUSH_LITERAL 0[221] RETURN[222] GOTO 286[224] PUSH_LITERAL 0[226] MOVE 14[228] RETURN[229] GET 14[231] LOAD_CONST 46[233] ADD[234] INTERNALCALLsay[236] LOAD_CONST 47[238] INTERNALCALLsay[240] LOAD_CONST 48[242] MOVE 15[244] GET 15[246] GET 1[248] INTERNALCALLlen[250] COM_LTETO[251] IFNE 279[253] GET 1[255] GET 15[257] LIST_INDEX[258] LOAD_CONST 49[260] ADD[261] GET 2[263] GET 15[265] LIST_INDEX[266] CONV_STR[267] ADD[268] INTERNALCALLsay[270] PUSH_LITERAL 1[272] GET 15[274] ADD[275] MOVE 15[277] GOTO 244[279] CALL 178[281] CALL 179[283] PUSH_LITERAL 0[285] RETURN[286] GOTO 415[288] RETURN[289] GET 5[291] LOAD_CONST 50[293] ADD[294] MOVE 5[296] INTERNALCALLtime[298] MOVE 8[300] GET 5[302] LOAD_CONST 51[304] COM_LT[305] IFNE 345[307] LOAD_CONST 52[309] MOVE 7[311] LOAD_CONST 53[313] GET 5[315] CONV_STR[316] ADD[317] INTERNALCALLsay[319] LOAD_CONST 54[321] GET 11[323] INTERNALCALLlen[325] INTERNALCALLrand[327] MOVE 9[329] GET 11[331] GET 9[333] LIST_INDEX[334] INTERNALCALLsay[336] GET 12[338] GET 9[340] LIST_INDEX[341] MOVE 10[343] GOTO 412[345] LOAD_CONST 55[347] INTERNALCALLsay[349] LOAD_CONST 56[351] NEG[352] MOVE 16[354] LOAD_CONST 57[356] MOVE 17[358] LOAD_CONST 58[360] MOVE 18[362] GET 18[364] GET 1[366] INTERNALCALLlen[368] COM_LTETO[369] IFNE 404[371] GET 2[373] GET 18[375] LIST_INDEX[376] GET 16[378] COM_GT[379] IFNE 395[381] GET 2[383] GET 18[385] LIST_INDEX[386] MOVE 16[388] GET 1[390] GET 18[392] LIST_INDEX[393] MOVE 17[395] PUSH_LITERAL 1[397] GET 18[399] ADD[400] MOVE 18[402] GOTO 362[404] CALL 224[406] GET 17[408] MOVE 14[410] CALL 229[412] PUSH_LITERAL 0[414] RETURN[415] GOTO 515[417] PUSH_LITERAL 0[419] MOVE 19[421] RETURN[422] LOAD_CONST 59[424] GET 19[426] ADD[427] INTERNALCALLsay[429] LOAD_CONST 60[431] MOVE 20[433] GET 20[435] GET 1[437] INTERNALCALLlen[439] COM_LTETO[440] IFNE 512[442] GET 1[444] GET 20[446] LIST_INDEX[447] GET 19[449] COM_ETO[450] IFNE 503[452] GET 20[454] GET 2[456] GET 20[458] LIST_INDEX[459] LOAD_CONST 61[461] ADD[462] LIST_STORE2[464] LOAD_CONST 62[466] GET 2[468] GET 20[470] LIST_INDEX[471] CONV_STR[472] ADD[473] INTERNALCALLsay[475] GET 2[477] GET 20[479] LIST_INDEX[480] LOAD_CONST 63[482] COM_GTETO[483] IFNE 499[485] LOAD_CONST 64[487] INTERNALCALLsay[489] CALL 224[491] GET 19[493] MOVE 14[495] CALL 229[497] GOTO 503[499] LOAD_CONST 65[501] MOVE 7[503] PUSH_LITERAL 1[505] GET 20[507] ADD[508] MOVE 20[510] GOTO 433[512] PUSH_LITERAL 0[514] RETURN[515] GOTO 645[517] RETURN[518] GET 3[520] LOAD_CONST 66[522] COM_ETO[523] IFNE 609[525] GET 4[527] LOAD_CONST 67[529] COM_ETO[530] GET 1[532] INTERNALCALLlen[534] LOAD_CONST 68[536] COM_GTETO[537] BOOL_AND[538] IFNE 552[540] INTERNALCALLtime[542] MOVE 4[544] LOAD_CONST 69[546] INTERNALCALLsay[548] INTERNALCALLtime[550] MOVE 6[552] GET 4[554] LOAD_CONST 70[556] COM_GT[557] IFNE 585[559] INTERNALCALLtime[561] GET 6[563] LOAD_CONST 71[565] ADD[566] COM_GT[567] IFNE 585[569] INTERNALCALLtime[571] MOVE 6[573] LOAD_CONST 72[575] LOAD_CONST 73[577] LOAD_CONST 74[579] LIST_MAKE3[581] INTERNALCALLpick[583] INTERNALCALLcommand[585] INTERNALCALLtime[587] GET 4[589] LOAD_CONST 75[591] ADD[592] COM_GT[593] GET 1[595] INTERNALCALLlen[597] LOAD_CONST 76[599] COM_GTETO[600] BOOL_AND[601] IFNE 607[603] CALL 67[605] CALL 68[607] GOTO 642[609] GET 7[611] IFNE 617[613] CALL 288[615] CALL 289[617] INTERNALCALLtime[619] GET 8[621] LOAD_CONST 77[623] ADD[624] COM_GT[625] IFNE 642[627] LOAD_CONST 78[629] INTERNALCALLsay[631] LOAD_CONST 79[633] GET 10[635] ADD[636] INTERNALCALLsay[638] LOAD_CONST 80[640] MOVE 7[642] PUSH_LITERAL 0[644] RETURN[645] GOTO 751[647] PUSH_LITERAL 0[649] MOVE 21[651] PUSH_LITERAL 0[653] MOVE 22[655] RETURN[656] GET 22[658] INTERNALCALLlowertext[660] MOVE 22[662] GET 22[664] LOAD_CONST 81[666] INTERNALCALLfindtext[668] IFNE 677[670] GET 21[672] LOAD_CONST 82[674] ADD[675] INTERNALCALLsay[677] GET 22[679] LOAD_CONST 83[681] INTERNALCALLfindtext[683] IFNE 721[685] GET 3[687] LOAD_CONST 84[689] COM_ETO[690] IFNE 704[692] LOAD_CONST 85[694] GET 21[696] ADD[697] LOAD_CONST 86[699] ADD[700] INTERNALCALLsay[702] GOTO 721[704] GET 1[706] GET 21[708] ADD[709] MOVE 1[711] LOAD_CONST 87[713] GET 21[715] ADD[716] LOAD_CONST 88[718] ADD[719] INTERNALCALLsay[721] GET 3[723] IFNE 748[725] GET 21[727] GET 1[729] LIST_IN[730] IFNE 748[732] GET 22[734] GET 10[736] INTERNALCALLfindtext[738] IFNE 748[740] CALL 417[742] GET 21[744] MOVE 19[746] CALL 422[748]


And this is what it looks like when you interact with an NPC running the triviabot DWAD.

Gambrinus logs in.
You say, 'join'
Rusty says, 'Aye, Gambrinus, I gotcha down.'
Rusty says, 'I'll wait 60 seconds for more players.'
Rusty yawns deeply.
Rusty dances a jig!
Rusty taps his foot impatiently.
Rusty dances a jig!
Rusty dances a jig!
Rusty says, 'Let's get this game started!'
Rusty says, 'Listen up, deez are the rules.'
Rusty says, 'Each round, I'll ask a question.'
Rusty says, 'Ye got 60 seconds to say the answer.'
Rusty says, 'First person who answers gets the point and we go to the next round.'
Rusty says, 'The game stops when a player gets 5 points or after 20 rounds, whichever comes first.'
Rusty says, 'Any questions?'
Rusty says, 'Too bad.'
Rusty says, 'Round 1'
Rusty says, 'What is the only sea on Earth with no coastline?'
You say, 'sargasso'
Rusty says, 'Aye, that's it Gambrinus'
Rusty says, 'Your score is now: 1'
Rusty says, 'Round 2'
Rusty says, 'What was first feature length animated film?'
You say, 'snow white'
Rusty says, 'Aye, that's it Gambrinus'
Rusty says, 'Your score is now: 2'
Rusty says, 'Round 3'
Rusty says, 'What was first feature length animated film?'
You say, 'snow white'
Rusty says, 'Aye, that's it Gambrinus'
Rusty says, 'Your score is now: 3'
Rusty says, 'Round 4'
Rusty says, 'In what year is Columbus credited with discovering the new world?'
You say, '1492'
Rusty says, 'Aye, that's it Gambrinus'
Rusty says, 'Your score is now: 4'
Rusty says, 'Round 5'
Rusty says, 'Which is the most abundant metal in the earth's crust?'
You say, 'aluminum'
Rusty says, 'Aye, that's it Gambrinus'
Rusty says, 'Your score is now: 5'
Rusty says, 'Attenion all, we have a winner!'
Rusty says, 'Gambrinus wins!'
Rusty says, 'Here be the final scores!'
Rusty says, 'Gambrinus: 5'