View Full Version : Sage Scripting
JamusPsi
03-02-2008, 10:04 AM
Tiny, tiny teaser as I continue work on a new scripting engine. The engine is intended to work such that the Atlas feature can run tiny scriptlets to make complicated moves from one room to another, where 'complicated' includes things like possible failure, buying a ticket, searching for an exit, etc.
$list=[a,b,c,d,e,f,g,h,i,j,k,l,m,n]
$concat = ""
for($x=0;$list[%x];$x==(%x + 1)) $concat==(%concat . %list[%x])
echo concat=%concat
Displays concat=abcdefghijklmn
for($x = 1.0;%x <= 10;$x==(%x + 1)) {
for($y = 1.0;%y <= 10;$y==(%y + 1)) {
if(%x / %y == 2.00) echo %x / %y = =(%x / %y)
}
}
Displays:
2.00 / 1.0 = 2.00
4.00 / 2.00 = 2.00
6.00 / 3.00 = 2.00
8.00 / 4.00 = 2.00
10.00 / 5.00 = 2.00
$foo = []
$foo.bar = baz
$foo.bang=$foo
echo %foo.bang[bang].["bang"].bar
Displays baz
As you may notice from the 'echo' lines, this language is designed to be thematically similar to wizard/sf scripts. It presently supports if, else, else if, while, for, break, continue, goto.
Occasional further tidbits will be posted here.
Deathravin
03-02-2008, 11:48 AM
What's wrong with Lich and zMud?
Drunken Durfin
03-02-2008, 11:55 AM
What's wrong with Lich and zMud?
Suddenly I am reminded of a quote from Demolition Man:
"Taco Bell was the only restaurant to survive the Franchise Wars. Now all restaurants are Taco Bell."
Renian
03-02-2008, 03:42 PM
What's wrong with Lich and zMud?
Neither of them are integrated right into PsiNet. zMud and Psinet are incompatible (as far as I know), and Lich and PsiNet don't get along well. That goes for the programs and their creators.
JamusPsi
03-02-2008, 10:00 PM
What's wrong with Lich and zMud?
:forehead: Oh my gosh. You're so right. What have I been doing for the last four years!? I've been completely wasting my time! Uniformly, the work I've been doing is no better than the alternatives.[i]
:fyi: Sarcasm aside? I get this question a lot. "What's wrong with lich?" "Why don't you just use perl/python/java/ruby/lua/fortran?" The fact is that you could make the same argument for lich and zmud (and PsiNet). What's wrong with telnet? What's wrong with wizard scripting alone?
My personal approach is this: a LOT of people, even lich users, tend to design their solutions using wiz/sf script. In the case of lich users, they often, I'm told, use the hybrid mode that allows them to intersperse ruby calls with sf script.
My personal opinion is that ruby itself is not really an ideal language for a send/expect type of application. Not that it's ineffective, but that sometimes it's convoluted. (I also, on a technical level, really don't enjoy calls in a virtual machine that block, or joining on their threads.) Same answer goes for the somewhat-retarded zmud triggers which rely on an extraordinarily unwieldy state machine (turn on this class, turn off that class, ad nauseum), and for c++ and C#.
I *could* write any script I wanted to in C# and build it into my personal copy of PsiNet. But I'd really rather not, because I find it largely ill-suited. I can't help but suspect that I'm not the only one who prefers (or would prefer) a more carved-down language option.
Any language I design will be, of course, less powerful than C#, or ruby, or perl, or python, (or or or). That's because real language theory is way, way over my head. But options already exist if you need that power- if not in lich, then you could write your own utility in whatever language you wanted. I'm not really looking to recreate that.
The idea is to provide more power with less hassle. (And on a technical level, as I mentioned before, to avoid certain synchronization issues that make me uncomfortable.)
That said, nobody is forcing anyone to use it. I heard the same "What's wrong with" [i]when I was writing PsiNet. Only that time the comparisons were to the spell 405, to the player's corner and forums and to AIM. I cannot tell you the number of people who loled when I told them about Spellup Auto, saying "Oh, I already use a script for that, I don't need that. What's wrong with using a script?"
Deathravin
03-03-2008, 02:01 AM
I didn't mean to be a jerk or anything. I was actually very curious.
I think a very good scripting language for GS was a long time ago. I can't even remember what that program was called anymore; But it does basically what you were saying people do with SF scripts using Lich code sprinkled in.
Honestly I just wish I could do 3 or 4 additonal things with Wizard scripting language. IF/Then/Else, Random variables, Known variables (such as Health, Health remaining etc so I don't have to type "Health"), and it not waiting for RT. Small stuff like that. So just some simple WSL syntaxed commands.
But that's just me.
I'm actually glad to hear that Jamus, the scripting in Lich is generally above what I'm willing to learn as far as the payback. But I often would like wizard scripting to have a number of more options. A happy medium would be awesome for me.
JamusPsi
03-03-2008, 09:17 PM
Honestly I just wish I could do 3 or 4 additonal things with Wizard scripting language. IF/Then/Else, Random variables, Known variables (such as Health, Health remaining etc so I don't have to type "Health"), and it not waiting for RT. Small stuff like that. So just some simple WSL syntaxed commands.
You're probably thinking wizbot. (Or maybe even JSE, which was also my creation.)
What you're describing is very close to what I'm making. %health as a read-only variable that represents your current character's health. Same with %mana and %mind. Put will wait for rt, but Forceput will not. (Similar to JSE again.)
Very simple sage scripts will (I plan) look *identical* to wizard script. From there you can use as much or as little of the extra power as you need.
JamusPsi
03-05-2008, 02:31 PM
What if one of your script sections could register a custom event that, when fired, would emulate a raised exception within your script?
For instance, you could catch your character being wounded or someone speaking, without having to put that in EVERY match/waitfor you ever did. When it occurred, it would simply break out of whatever you were doing.
Concept syntax, subject to change:
function processroom() {
$speakingevent = RegexEvent("SomeoneSpoke", "^(?!You )(\w+) (say|exclaim|ask)s, \"(.+\"$")
try {
loop:
$done = false
while(%done ^ true) $done = attack_critter(%critter)
$critter = find_next_critter()
if($critter) goto loop
} catch("SomeoneSpoke", "Wounded") {
if(%event == Wounded) {
$continue = handle_wounds()
if($continue) goto loop
throw "AbortHunt"
} else if(%Event == "SomeoneSpoke") {
put wave
pause 1
throw "AbortHunt"
}
}
}
Deathravin
03-05-2008, 03:38 PM
YES I was thinking of Wizbot! I couldn't figure out the name LOL.
That'd be cool to have that sort of catch.
How about adding an ability to say something based on a jaberwacky responce. LOL. Input what they said and their tone into Jaberwacky.com and let it respond with the responce.
LOL! Just tried it out:
Me: What's wrong with you?
JW: Nothing. What's wrong with you?
ME: Hey, that was my kill!
JW: It was?
ME: Yes it was!
JW: No it wasn't.
ME: I was chasing it, anybody could see that.
JW: You could see what?
ME: Whatever
JW: Don't whatever me.
That might get you in a bit of trouble LOL.
Celephais
03-05-2008, 04:14 PM
YES I was thinking of Wizbot! I couldn't figure out the name LOL.
That'd be cool to have that sort of catch.
How about adding an ability to say something based on a jaberwacky responce. LOL. Input what they said and their tone into Jaberwacky.com and let it respond with the responce.
LOL! Just tried it out:
Me: What's wrong with you?
JW: Nothing. What's wrong with you?
ME: Hey, that was my kill!
JW: It was?
ME: Yes it was!
JW: No it wasn't.
ME: I was chasing it, anybody could see that.
JW: You could see what?
ME: Whatever
JW: Don't whatever me.
That might get you in a bit of trouble LOL.
... and fucked around with smarterchild, it could respond to players as well as GM tests... of course it's a prick "Hey can I get some strength?" "I don't know... can you?")
They do respond well for the type of stuff that people "test" bots with "Hey, are you there?" works fine... but you eventually end up just talking like Foxs.
JamusPsi
04-15-2008, 08:33 AM
$counter = counter()
echo counter is %counter
%counter.print()
%counter.increment()
%counter.print()
%counter.decrement()
%counter.print()
%counter.setvalue(3)
%counter.print()
function counter() {
function echo() {
echo %this.number
}
function inc() {
$this.number = %this.number + 1
}
function dec() {
$this.number = %this.number - 1
}
function set(num) {
$this.number = %num
}
$result = []
$result.number = 0
$result.print = &echo
$result.increment = &inc
$result.decrement = &dec
$result.setvalue = &set
return $result
}
Those familiar with object-oriented javascript will likely see the similarity. In the language i'm designing, functions may be nested (and become private thusly). However, a reference to those functions may be returned, or returned within another variable, and then those references can be called from that variable.
In the above example, I call counter(), which has several sub-functions. counter() creates a new variable, initializes it with its methods and data (0), and then returns it. The calling section stores the resultant counter object and invokes its methods.
SpiffyJr
04-15-2008, 11:17 AM
I've only been using Ruby for Lich for a month (maybe less) or so now but I greatly enjoy its features. I'm program embedded systems for a living so my knowledge is slighter higher than your average Gemstone user. I would wager there's very few people that are able/willing to fully learn Ruby. I wouldn't use your engine for generic scripts because I use Ruby but I do add many many many rooms onto Atlas and I'd be more than willing to write up a few quick scripts for travel purposes.
BigWorm
04-15-2008, 12:07 PM
$counter = counter()
echo counter is %counter
%counter.print()
%counter.increment()
%counter.print()
%counter.decrement()
%counter.print()
%counter.setvalue(3)
%counter.print()
function counter() {
function echo() {
echo %this.number
}
function inc() {
$this.number = %this.number + 1
}
function dec() {
$this.number = %this.number - 1
}
function set(num) {
$this.number = %num
}
$result = []
$result.number = 0
$result.print = &echo
$result.increment = &inc
$result.decrement = &dec
$result.setvalue = &set
return $result
}
Those familiar with object-oriented javascript will likely see the similarity. In the language i'm designing, functions may be nested (and become private thusly). However, a reference to those functions may be returned, or returned within another variable, and then those references can be called from that variable.
In the above example, I call counter(), which has several sub-functions. counter() creates a new variable, initializes it with its methods and data (0), and then returns it. The calling section stores the resultant counter object and invokes its methods.
So that's like a hacked together version of a constructor for a class? You mentioned OOP, why not have real classes? Also, what's with all the sigils? I write a lot of perl, so I'm used to them, but they seem likely to confuse someone new to the language. At the very least, you should consider using the same sigil for the LHS and RHS of assignment.
JamusPsi
04-15-2008, 05:51 PM
Since you write perl, the answers to your questions are somewhat easy. Similar to perl, my language has only a few data types- scalar and table (think hash). In expressions it has an additional term type, list, but if assigned to a variable a list becomes a table with indexes "0", "1", "2"...
A constructor in perl is very similar to mine, except instead of assigning the methods directly into a table, it uses the bless keyword to enchant one.
Javascript and perl both don't have 'real' classes, in as much as they don't have any sort of strict type checking. What they do do is provide mechanisms to create objects with behaviors, and it is this I have emulated.
Creating 'real' classes would involve changing the rather simplistic language concepts I'm using, and a great deal more knowledge of lexing/compiling than I presently possess. I also don't think it's necessary- the language I'm designing is meant to be as forgiving as possible.
As for sigils, there are presently 3: %, $, and &.
& is easiest. Rather than looking for a variable in the present scope, this looks for a function visible to the current function. It is primarily to prevent name collisions. (I could make functions into automatically generated variables within the scope, but then you would require a sigil to call them, instead of just to reference them.)
Remember that my language, being unstrict, treats barewords as literal strings.
$ and % both refer to variables, and both support some extra syntax not in my examples. First, they support both [] and . indexers. foo.bar is identical to foo["bar"]. They also both allow for %{foo.bar} which ensures that the entire {} text is used as the address of the variable, and fails otherwise.
The difference between them is their meaning. In general, % means the content of the variable, and $ means the variable itself. For instance in:
echo Hello %world.
the variable world is immediately dereferenced into a string and inserted into the expression given to echo. In:
echo Hello $world.
the variable world is passed as a variable to echo. The difference is subtle but crucial.
Here's a more likely example (one of my tests):
$x = 0
echo Before first call, $x is %x
f(%x)
echo After first call, $x is %x
f($x)
echo After second call, $x is %x
function f(y) {
echo f got %y
$y = 3
echo f set $y to %y
}
In this code the function f displays what it received, then sets it to 3 and says so. Before and after each call the script displays the value of x.
f(%x) passes the value of the variable x, which is assigned into a new variable y. y can be modified, but when returning, x is not modified.
f($x) passes the variable x, which is renamed to y within the function. When y is modified, x is modified as well.
The output of the above code:
Before first call, $x is 0
f got 0
f set $y to 3
After first call, $x is 0
f got 0
f set $y to 3
After second call, $x is 3
>Script ended. (Ended)
Were my language truly compiled I would be able to tell by context whether or not I was intending to use the value or the reference, like perl does. Instead it uses an extra sigil to denote the difference.
In assignments:
%x = %y
is meaningless, becoming the literal expression "3 = 4". This is another case where the difference between a reference and a value is important.
$x = %y
copies the value of y into x.
$x = $y
copies the variable reference y into x, making them thereafter the same variable.
$x = %y + 1
copies the evaluation of 1 plus the value of y into x.
$x = $y + 1
Fails with an error- variable references are not arithmetic. (It's like doing math on a pointer in c or perl.) I am presently considering making an exception here just to make it easier: when arithmetic is attempted on a variable reference, automatically dereference it.
Example:
$x = 1
$y = 3
echo $x is %x, $y is %y
$x = %y
$y = 5
echo $x is %x, $y is %y
$x = $y
$y = 10
echo $x is %x, $y is %y
Outputs:
$x is 1, $y is 3
$x is 3, $y is 5
$x is 10, $y is 10
It IS a little more complicated, but it's by necessity in order to support reference types like I wanted.
BigWorm
04-15-2008, 07:16 PM
Sorry if my last post was snarky. I didn't intend that, but rereading it seems like it may have been. I was mostly just curious about why you made the decisions that you did. Your explanations make sense; basically, $ is pass by reference and % is pass by value.
JamusPsi
04-15-2008, 07:38 PM
Thanks! Trust me, feedback at this stage is appreciated. I'm working now on designing the event/match system, which is somewhat more complicated.
One thing I'm trying to put together in my head is exception-raising. Essentially you'd register an event, like:
$deathevent = RegexEvent("DeathID", "^It seems you have died, my friend\.")
EnableEvent($deathevent)
try {
call the main activity of the script
} catch ($event, DeathID, List, Of, Other, EventIDs) {
if(%event.ID == "DeathID") {
echo "You died."
exit
}
}
There would also be some better built-in event types like mana changes, status changes, new rooms, psinet activity, speech, thoughts, etc. The idea being that if you're catching an event you can interrupt whatever was happening to move back up the call stack.
You could also nest them- in this example, whatever portion of the script was active when you died could also catch the death, record what it was doing when the death occurred, and then throw the event again, moving it further up the chain.
Some events would be errors if not caught, terminating the script, while others would be optional- a monitoring script could send an event to one or more other running scripts, which they could use or ignore.
I think this is a pretty innovative way of enhancing the generic send/expect mechanisms we've come to expect, without the need for a state machine or a real message loop.
JamusPsi
04-28-2008, 06:06 AM
I hadn't realized I had gone so long without an update here on the progress of the new scripting engine. This one's going to be pretty big.
First, a little background, and a little review. I'm writing the scripting engine not to be efficient but to be extremely controllable and extensible. It is not intended to be super-powerful but rather to create a simple means to extend send/expect communication patterns.
Send/expect: I send this, then I expect one of these responses, which I respond to by sending this, then ...
Because this is a pretty generic idea, I decided fairly early on that the engine itself was going to be completely abstracted. This means that while the syntax and core language is well-defined, the actual statements and functions that make it do something are implemented by the host application.
(English: I can use this scripting engine in other programs, not just PsiNet/Sage.)
This, it appears, is pretty much done.
The language has settled on the following basic constructs:
if, else, else if, while, for, break, continue. These have C-like syntax, but are somewha stricter- only one of these keywords (except break and continue) may appear on a single line, and while you may have single-line controls:
bar()
if(%run_foo)
foo()
bar()
--you may not nest any other controls within their contents without using {}s.
Scripts are composed of functions which may have subfunctions. The root, unnamed funciton of a file is what is run when the script is executed. A function may access its child and sibling functions, but not cousins or grandchildren. So for instance:
I can only call a here.
function a() {
I can call b here.
But I cannot call c.
function b() {
I can call a, b, c, and d here.
function c() {
I can call a, b, c, d here.
}
}
function d() {
I can call a, b, and d here
But I cannot call c.
}
Nested functions keep the namespace clearer when designing complex scripts by hiding name conflicts. When a function is called it obtains its own scope (variable space) and so they won't (and can't) overwrite or access variables outside of their function, with a few exceptions.
Functions have parameters which can be passed. In addition, there are several special variable tables:
local - A space for variables which need to be accessed from anywhere within the script, for as long as it is running.
global - A space for variables which need to be accessed from any script, and which should persist even after the script stops running.
parentscope - A way to reference variables which belong to the method which called the currently running method.
The language supports both an event-driven and a procedural mode which can be intermixed. For instance, you can say something like:
Send a command
Wait for a response...
Continue
Or, you can say:
Interrupt what I'm doing if this happens.
(Go about other business)
In the event that the event occurs, the script immediately jumps back up the call stack to the relevant catching block, aborting any waits or holds that may have otherwise existed.
This allows a script to respond to edge cases without having to code considering every possibility at EVERY juncture- you can simply register for notification and interruption in advance.
The engine is written such that it is relatively simple to add additional conditions on the fly- for instance, to add a condition (for either waiting on or event-driven watching) for stamina being reduced, increased, changed, run out, reached full, dropped below an arbitrary point, etc.
In addition to this flexibility, it has the (largely untested) ability to insert 'live' variables into the script space- for instance, %mana would be an automatically-updated value of your mana based on the information the front end receives (and psinet has parsed).
But because of the parsing enhancements present in PsiNet, we can do things a little closer to...
echo My strength bonus is %me.stats[str].bonus.
if(%me.wounds.count > 0) do_some_healing()
put cure_commands[%me.wounds[0].location]
$exitnum = rand(0, =(%here.exits.count -1))
put %here.exits[%exitnum].command
Because the engine is being built into PsiNet/Sage, it will be able to have access to a great deal of information about the game environment, including that which Sage itself provides- like your magic status, or atlas information about where you are or need to go.
Things left to do before I set out implementing the engine within PsiNet: a throw keyword to fire off custom events. Method importing from one script to another (makes it possible to make library-esque collections of functions).
There is currently no goto keyword, primarily because the engine does some pre-processing on each method to improve efficiency at runtime (calling it compiling would be generous though), meaning that gotos would only be meaningful within a single function in the script- you could not goto into another function. Still on the table, but of dubious usefulness in this context.
Comments, questions, and suggestions are welcome.
JamusPsi
04-29-2008, 08:53 PM
Method importing from one script to another (makes it possible to make library-esque collections of functions).
This is now done.
The import keyword loads another script, and allows the method using it to access the imported script's top-level functions as though they had been declared within that method.
For instance, if you have library.ss which contains function imp():
function foo() {
import library.ss
imp()
}
works, but:
function foo() {
import library.ss
}
imp()
Does not. However, imported functions use the same rules as other nested functions, so:
import library.ss
function foo() {
imp()
}
Works just fine.
Circular imports are ok as well. Because loaded scripts are cached in compiled data, in the event that an imported script changes, running the main script will trigger a reload of both scripts automatically. However, if only the main script changes, the imported script will not be reloaded.
To ease development of complex code, scripts have some error-checking built in beforehand, including simple syntax errors. They also check all function calls at compile time to verify that they are implemented, either by the script, the engine itself, or an imported script. That means if one of your scripts breaks another, you will find out when you try to run it, and not later when it tries to run the missing function.
It does however create one caveat, and it is a factor of the engine that I cannot correct. Because I wanted both to be able to use arbitrary strings within a script without generating an error, but also to have the advanced syntax I do, certain things are slightly more complicated. Here's an example:
function test() {
echo test() is running
}
This function is an infinite loop, as it calls itself trying to find what it should echo. You can change it to:
function test() {
echo "test() is running"
}
Which prevents the function call, but echos the "s as well. To echo it without quotes, you must evaluate it:
function test() {
echo =("test() is running")
}
(This essentially performs a string concatenation of only one string and returns the value.)
It's a little bit of a hassle, but the added power (and the laxness otherwise) makes up for it, I think.
Onwards to throw!
JamusPsi
05-03-2008, 04:07 AM
Throw is now working.
function event(name) {
$event = []
$event.name = %name
return $event
}
try {
for($x=0;%x < 100;$x=%x + 1) {
echo x=%x
if(%x == 10) throw(event("HitTen"))
}
} catch ($event, HitTen) {
echo I hit ten.
echo %event.name
}
echo End script.
This prints the numbers 0-10, then prints "I hit ten" and "HitTen". If you change the event name (within the throw) or comment out the if, it counts to 99, then prints "End script."
JamusPsi
05-18-2008, 09:02 PM
I made a bit of a revision to the system for inter-script operability.
There are now two methods of importing:
imported.ss
function imp() {
echo Imp
}
test.ss
import imported.ss
imp()
When test.ss is run, it gains access to imported.ss's top-level functions and can call them as though they were declared in place of the import directive.
The best part is that these references are live. That means that if you have imported.ss open in your editor and change the function imp() WHILE test.ss is running, then the next time test.ss calls imp(), your changes will be reflected, WITHOUT restarting test.ss!
But because code designed in this engine should, ideally, be highly modular, I've added another method of importing- the import function.
test2.ss
$imp = import(imported.ss)
%imp.imp()
This code does precisely the same thing as test.ss, except that the functions are not declared in the script, but instead are returned in a table that you can store. This even allows for reassignment or tests or evaluated references; all these examples execute the imported function:
$temp = "imp";
%imp[%temp]()
%imp[=("im" + "p")]()
$copy = []
$copy.f = $imp.imp
%copy.f()
if($imp.blah)
%imp.blah()
else if($imp.imp)
%imp.imp()
And these references are ALSO live- changes in the imported function will be reflected when they are next called. An error is thrown if the imported function no longer exists in the file.
..and as I type this, it occurs to me that my change may have done something else as well.. and a quick test confirms it!
test3.ss
while(true) {
echo Waiting for go.
waitfor go
f()
}
function f() {
echo foo
}
This code calls f() every time it receives the word go. If the function f is modified between 'go's, the UPDATED version is run even without restarting the script.
Cool.
JamusPsi
05-19-2008, 12:26 AM
One more quick update- after some deliberation, I added labels and goto. Labels are restricted such that they may contain only letters and numbers and must start with a letter. Precisely, they must match:
^\s*[a-zA-Z_][a-zA-Z0-9_]*:\s*$
Each function can have its own function with the same name without problem. Duplicate labels within the same function will cause an error. LabelError is a special label, as in wizard, which will match unknown gotos- this is for legacy reasons, for novice or beginning scripters.
You may NOT goto from one function into another; to transfer control between functions you must call or return. For other cases you may use the try/catch/throw mechanism to indicate a special change further up the call stack.
Powered by vBulletin® Version 4.2.5 Copyright © 2024 vBulletin Solutions Inc. All rights reserved.