Passing wildcards to script procedure as arguments

Posted by Cadari on Sun 26 Aug 2007 04:27 PM — 31 posts, 121,409 views.

#0
Greetings again. I'll continue bothering you with simple questions, if you don't mind.

Say, I need to make a trigger firing on the line:

You attack a (ghost|ghast|spectre).

and sending the matching wildcard to the script procedure, like this:

def on_attack(target):
    world.Note('You attack a %s!' % target)

So you'll see 'You attack a ghost!'. The trigger:

  <trigger
   enabled="y"
   match="^You attack a (ghost|ghast|spectre)\.$"
   regexp="y"
   send_to="12"
   sequence="100"
  >
  <send>on_attack(%1)</send>
  </trigger>

won't work, as it passes ghost as a variable name, not as a string:

Traceback (most recent call last):
  File "<Script Block 64>", line 1, in <module>
    on_attack(ghost)
NameError: name 'ghost' is not defined

Anything I'm missing?

EDIT: Forgot to add: Send to: World and working with world.GetTriggerWildcard isn't it. It must send to Script.
Amended on Sun 26 Aug 2007 05:24 PM by Cadari
USA #1
  <trigger
   enabled="y"
   match="^You attack a (ghost|ghast|spectre)\.$"
   regexp="y"
   send_to="12"
   sequence="100"
  >
  <send>on_attack("%1")</send>
  </trigger>

The %1 isn't a string, it's a literal replacement of the wildcard. So for your example, you were calling on_attack( ghost ) instead of on_attack( "ghost" )
#2
You can't imagine how many hours of futile attempts to make it work you just saved me. Thanks!
#3
There's some kind of a memory leak somewhere in the bowels of the connection between MUSHclient and Python's Active Scripting implementation that is triggered by "Send to Script" (probably the code object isn't free'ed but that's not something I know how to test).

If you are writing a script function anyway you might as well use the script call feature:
<triggers>
  <trigger
   enabled="y"
   match="^You attack a (ghost|ghast|spectre)\.$"
   regexp="y"
   script="on_attack"
   sequence="100"
  >
  </trigger>
</triggers>


Which means your function must look like:
def on_attack(name, line, wildcards):
    world.Note("You attack a %s!" % wildcards[0])


HTH. HAND.
Amended on Mon 27 Aug 2007 04:28 AM by Isthiriel
#4
Thank you, Isthiriel, but unfortunately, I have to use Send to: Script. My example above is just an example, not the real routine.
#5
Out of curiousity, why?
USA #6
Only thing I can think of to have Send to Script necessary would be if you needed to dynamically change what the trigger does. In Lua, this can be done fairly easily with a loadstring() call, and I'm sure Python has a way to do it. Usually calling a script has much more versatility than the send field, and I can't really seem to think of a way to require the use of the send field.
#7
Python has at least two.

The eval() function which evaluates a string expression, and the exec statement which executes a string.

That is:
Python 2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1310 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> print eval("4 + 5")
9
>>> exec "print 'Hello world!'"
Hello world!
>>>


I think you can also compose functions on the fly (like LISP macros), though that is trickier.
Amended on Mon 27 Aug 2007 04:33 PM by Isthiriel
#8
Maybe it's just my ignorance of certain MUSHclient/Python features, but I use Send to: Script most of time.

I play IRE MUD. There are numerous afflictions (blindness, deafness, limb breaks, curses and on and on). I could setup a script procedure for each one of them, like

def on_blindness():
    global afflictions    
    afflictions['blindness'] = True    
    world.Note('Blindness!')

Then add an equal number of corresponding triggers and use Send to: World. But as I said, there are lots of possible afflictions, so obviously it's better to have something like:

def on_affliction(affliction):
    global afflictions    
    afflictions[affliction] = True    
    world.Note(affliction)

And Send to: Script on_affliction('blindness').

This whole topic was about the following thing (some examples):

<triggers>
  <trigger
   enabled="y"
   group="knighthood_afflictions"
   match="\w+ (strikes|cuts|slashes) the tendon above your (left|right) heel\, and you scream in agony as it\'s completely severed\.$"
   name="severed_tendon"
   regexp="y"
   send_to="12"
   sequence="100"
  >
  <send>
    afflicted('severed_%s_tendon' % '%2')
  </send>
  </trigger>

  <trigger
   enabled="y"
   group="knighthood_afflictions"
   match="Your (left|right) leg is completely slashed through\, which immediately plops to the ground\, leaving only a bloody stump\.$"
   name="amputated_leg"
   regexp="y"
   send_to="12"
   sequence="100"
  >
  <send>
    afflicted('amutated_%s_leg' % '%1')
    afflicted('stun')
  </send>
  </trigger>
</triggers>

If there's any way to do this without Send to: Script, that would mean I'm just dumb :) And also I would be very grateful, as Send to: Script looks ugly when you look through the XML file with triggers :)
Australia Forum Administrator #9
Quote:

But as I said, there are lots of possible afflictions, so obviously it's better to have something like:



def on_affliction(affliction):
    global afflictions    
    afflictions[affliction] = True    
    world.Note(affliction)


And Send to: Script on_affliction('blindness').


You could put the affliction into the trigger name (label), and then the script could simply use that. Something like this:


def on_affliction(name, line, wildcards):
    global afflictions    
    afflictions[name] = True    
    world.Note(name)


Now each (appropriate) trigger can simply call on_affliction, without having to use send-to-script. You may have a problem if you have an "on" affliction and an "off" affliction, but the name could convey that, eg.

Turned on: on_blindness
Turned off: off_blindness

The script strips off the first part (on or off) and uses that to decide whether the affliction is going on or off. The rest of the word is which affliction.

Here is another way of achieving that. In the trigger use "send to variable", This lets you send a nice long string to a single variable, without the overhead of invoking the script engine parser. For example:


<triggers>
  <trigger
   custom_colour="2"
   enabled="y"
   match="You are going mad."
   script="on_affliction"
   send_to="9"
   sequence="100"
   variable="affliction"
  >
  <send>madness</send>
  </trigger>
</triggers>


This puts the word "madness" into the MUSHclient variable "affliction" and then calls the script "on_affliction". That can then pull "madness" out of the variable and set the appropriate script language variables. This is what it might look like in Lua:


function on_affliction (name, line, wildcards)
  afflictions [GetVariable ("affliction")] = true
end -- function

Amended on Mon 27 Aug 2007 10:19 PM by Nick Gammon
USA #10
Another possibly way to do this is to have the script act differently with a bunch of if statements or a case switch. My Python skills are horrid, so I'll show an example in Lua, which is easy enough to read.


<triggers>
  <trigger
   enabled="y"
   group="knighthood_afflictions"
   match="\w+ (?:strikes|cuts|slashes) the tendon above your (left|right) heel\, and you scream in agony as it\'s completely severed\.$"
   name="severed_tendon"
   script="affliction"
   regexp="y"
   send_to="1"
   sequence="100"
  >
  </trigger>

  <trigger
   enabled="y"
   group="knighthood_afflictions"
   match="Your (left|right) leg is completely slashed through\, which immediately plops to the ground\, leaving only a bloody stump\.$"
   name="amputated_leg"
   script="affliction"
   regexp="y"
   send_to="1"
   sequence="100"
  >
  </trigger>
</triggers>

function affliction( sTrig, sLine, wildcards[] )
  if sTrig == "severed_tendon" then
    afflicted( 'severed_%s_tendon'..wildcards[1] ) -- Lua starts arrays off at 1, not 0
  elseif sTrig == "amputated_leg" then
    afflicted('amutated_%s_leg'..wildcards[1])
    afflicted('stun')
  end
end -- affliction

Amended on Mon 27 Aug 2007 10:58 PM by Shaun Biggs
#11
Nick, Shaun, many thanks for explaining the things for me, though some questions arise.

In fact, my first version was identical to Shaun's one, but I thought it would work slower with all the if-elif statements, so I switched to what I posted above.

Now the question. Given the fact you're dealing with more than hundred possible afflictions, what route is the best? Execution speed issue is very important, as I have about two hundred triggers for now, and the numbers are growing.

P.S. As promised, I admit I'm dumb :)
USA #12
Just keeping it out of 'send to script' will help execution speed immensely. MUSHclient's trigger matching is very fast, and I have had a few hundred triggers on a P120 with no choppiness. A simple case statement should take care of the actual grunt work, and Python's script engine is pretty fast on it's own. It will be a long list, but manageable.
#13
Understood. Thank you.
#14
You know I just downloaded MUSHClient and like the guy above it confuses me to no end. The main problem is just simply communicating with my LUA scripts.

Why is it so complicated to do afflict("illness")?

Using the label to send the affliction works, but the only problem is you also have a trigger that cures the same affliction...

How do I do this without having to creating a bunch of extra code for no reason?

I have all the afflictions in a table.

All I want to do is send the affliction name to:

-- Afflict function
function afflict(affliction)
if afflicted[affliction] == false then
afflicted[affliction] = true
ColourNote("red", "black", "Afflicted: ", "khaki", "black", affliction)
end
end -- afflict

-- Unafflict function
function unafflict(affliction)
if afflicted[affliction] then
afflicted[affliction] = false
ColourNote("lime", "black", "Cured: ", "khaki", "black", affliction)
end
end -- unafflict

There are no wildcards on most of the triggers.

I am just not understandstanding the interface of Mushclient. Why would I want to have to create other variables when I am already using the LUA table? sigh
Australia Forum Administrator #15
I'm sorry, I don't really understand your question. It would help if you showed the trigger(s) in question.

Quote:

Why would I want to have to create other variables when I am already using the LUA table?


What other variables? If you could show the entire thing you are attempting, along with sample output, it would become clearer.
#16
Well for example in the following trigger:

^Blah beats you over the head with a stick\.$

Let's say that gives stupidity.

Now how do I communicate that to the afflict function?

You stated, "You could put the affliction into the trigger name (label), and then the script could simply use that."

I tried putting stupidity in the label, but that doesn't work because you also have a trigger that cures it too and they both can't have the same id/label.

I need to send stupidity to the afflict and unafflict functions which set the state of afflicted.stupidity to true or false and substitute the triggers with Afflicted: stupidity or Cured: stupidity.

Another person mentioned putting 30 if statements, but that seems silly when I already know the argument I am sending and a giant waste of time.

As far as the variable part that is simply what you stated:

---------------------

Here is another way of achieving that. In the trigger use "send to variable", This lets you send a nice long string to a single variable, without the overhead of invoking the script engine parser. For example:


<triggers>
<trigger
custom_colour="2"
enabled="y"
match="You are going mad."
script="on_affliction"
send_to="9"
sequence="100"
variable="affliction"
>
<send>madness</send>
</trigger>
</triggers>



This puts the word "madness" into the MUSHclient variable "affliction" and then calls the script "on_affliction". That can then pull "madness" out of the variable and set the appropriate script language variables. This is what it might look like in Lua:


function on_affliction (name, line, wildcards)
afflictions [GetVariable ("affliction")] = true
end -- function

------------------

Is that really how you have to do it? Pass it to another variable first?
Australia Forum Administrator #17
Well no, you don't have to do it that way. That was a suggestion to minimize writing. Another approach which avoids the (single) intermediate variable, but means more typing, is this:


<triggers>
  <trigger
   enabled="y"
   match="You are going mad."
   send_to="12"
   sequence="100"
  >
  <send>
afflict ("madness")
</send>
  </trigger>
</triggers>


That simply calls the function you suggested (afflict) and passes the word "madness" to it, thus avoiding any intermediate variables. I suppose this is neater, but the trigger is very slightly more complicated.

Then your unafflict could be done like this:


<triggers>
  <trigger
   enabled="y"
   match="You are feeling sane."
   send_to="12"
   sequence="100"
  >
  <send>
unafflict ("madness")
</send>
  </trigger>
</triggers>


And then ditto for the other afflictions. I don't think you can make it much simpler than this, because somehow you need to translate the affliction messages into an affliction name, and the triggers seem the simplest way of doing it.

One possible alternative would be to simply have a trigger that matches everything (ie. "*") and then use a Lua table-lookup to map the line to the affliction, eg.


affliction_on_table = {
  ["You are going mad."] = "madness",
  ["You feel sleepy."] = "sleep",
  }

affliction_off_table = {
  ["You are feeling sane."] = "madness",
  ["You feel awake again."] = "sleep",
  }


If you go down that path make sure you give it a low sequence number (eg. 50) and mark the trigger "keep evaluating" because you will still want other triggers to match too.



#18
<triggers>
<trigger
enabled="y"
group="Venoms"
match="^The idea of eating or drinking is repulsive to you\.$"
omit_from_output="y"
regexp="y"
send_to="12"
sequence="100"
variable="affliction"
>
<send>afflict("anorexia")</send>
</trigger>
</triggers>

Okay now my afflict function won't work. It doesn't do the colournote. It does change afflicted.anorexia to true though. Why isn't ColourNote working when I test the trigger?

function afflict(name, line, wildcards)
afflicted[name] = true
ColourNote("red", "black", "Afflicted: ", "khaki", "black", name)
end

#20
Ah ha! Script (after omit). No wonder.
#21
I hate to be a bother, but how do you refer to the line being received that the trigger is matching?

For example, when the trigger fires I want to check the length against what was received, then check it against another string for a match.

This is for a certain type of affliction you can get on IRE that will put in scattered * throughout the pattern.

Example:

A prick*y stingi*g ove**omes your bo*y, fading away into numbness.

The line must always be 66 but how do you refer to line?

Like in zmud/cmud you use %line.
#22
Sorry to clarify I meant the string.len will always be 66.
Australia Forum Administrator #23
The problem with the prickly thing has been discussed before, see:

http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=3525

However to answer your direct question, you could feed the length to the function, like this:


afflict("anorexia", string.len ("%0"))


Wildcard %0 is the entire matching text. This is not necessarily the entire line if you are using regular expressions, but will be if not.

However that suggestion has a potential problem if there is a quote in the text, and it would be better like this:


afflict("anorexia", string.len ([[%0]]))


The [[ ... ]] construct is a Lua "long quoted string" and allows for imbedded quotes (but not however imbedded [[). To work around that you can use this form:


afflict("anorexia", string.len ([===[%0]===]))


And hope they don't put [===[ on any line.

To be really certain, when passing the matching line to a script, you should really use a script function in a script file rather than send-to-script. Then the matching line is supplied as an argument, and it doesn't matter how many quotes are in it.

I note your earlier comment:

Quote:

I tried putting stupidity in the label, but that doesn't work because you also have a trigger that cures it too and they both can't have the same id/label.


It isn't too hard to make different labels, eg. on_stupidity and off_stupidity, and then have the script omit the on_ or off_ parts before looking them up in the table.



#24
I apologize for referring to something that was discussed before, but that thread title isn't exactly discriptive and I did a search for paralysis.

However, I had figured it out later after I got an error which told me that %0 referred to the entire trigger text. :-)

Anyway, all I did was just put this script in that trigger..

local str = [===[%0]===]
if string.len(str) == 66 then
str = string.gsub(str, "*", ".")
if string.match("A prickly stinging overcomes your body, fading away into numbness.", str) then
afflict("paralysis")
else
ColourNote("yellow", "black", "Paralysis Illusion!!")
end
end

It works perfectly. I changed it to use your suggestion on the quoting though. See anything wrong with it? I admit I'm a novice yet with LUA.
Australia Forum Administrator #25
That seem a clever way of making the periods match on the underlying text. Technically the period won't quite match correctly, you should probably replace "." by "%." before doing the string.match. Also the test for length 66 now becomes redundant, except to save a bit of speed, however the Lua pattern matching is very fast.

Perhaps if you did this:


str = string.gsub (str, ".", "%.")  -- periods should match exactly
str = string.gsub (str, "*", ".")   -- convert wildcards
str = "^" .. str .. "$"    -- anchor it


Then you don't need the test for the length.
Australia Forum Administrator #26
Actually if you are turning untrusted input into a regexp, you should probably fix up all special characters, as documented under string.find, like this:


str = string.gsub (str, "[$().%%%[%]*+%-?\%^]", "%%%1")  -- fix up special characters


Otherwise, if the string you are testing for "A prickly stinging overcomes your body, fading away into numbness" is something like: "blah blah [ ... blah" (66 character long) then the regexp will fail with an unmatched opening bracket.
Amended on Fri 23 Jan 2009 03:45 AM by Nick Gammon
#27
Quote:
e That seem a clever way of making the periods match on the underlying text. Technically the period won't quite match correctly, you should probably replace "." by "%." before doing the string.match. Also the test for length 66 now becomes redundant, except to save a bit of speed, however the Lua pattern matching is very fast.

Perhaps if you did this:


str = string.gsub (str, ".", "%.") -- periods should match exactly
str = string.gsub (str, "*", ".") -- convert wildcards
str = "^" .. str .. "$" -- anchor it



Then you don't need the test for the length.


No that doesn't work because it doesn't catch things like misspelling a word.

If I do, as you posted and include:

Quote:
str = string.gsub (str, ".", "%.")


It then ends up matching on a string like the following:

Quote:
A prickly stinging ov*rcom*s your body, f*d*ng aw*y into nubbne*s.


"nubbness" is incorrect.

Just doing str = string.gsub(str, "*", ".") matches correctly and will flag nubbne*s as an illusion for misspell.

I'm just replacing the stars * with . which represents any single character so it matches with the regex pattern.

A prickly stinging ov*rcom*s your body, f*d*ng aw*y into nubbne*s.

turns into...

A prickly stinging ov.rcom.s your body, f.d.ng aw.y into nubbne.s.

and I am matching it against...

A prickly stinging overcomes your body, fading away into numbness.

which has a period at the end of it.
Australia Forum Administrator #28
Oops.

I meant:


str = string.gsub (str, "%.", "%.")  -- periods should match exactly


My initial suggestion would have changed every character into a dot, which would have matched anything that length. The amended version changes dots in the original to %.



str = "A prickly stinging ov*rcom*s your body, f*d*ng aw*y into nubbne*s."

str = string.gsub (str, "%.", "%.")  -- periods should match exactly
str = string.gsub (str, "*", ".")   -- convert wildcards
str = "^" .. str .. "$"    -- anchor it
if string.match ("A prickly stinging overcomes your body, fading away into numbness.", str) then
  Note ("paralysis")
else
  Note("Paralysis Illusion!!")
end


That successfully returns "illusion" and for the bad string, and "paralysis" for the correct one.

However my other suggestion almost worked too. ;)

However I shouldn't have put the asterisk into it.

Thus this works:



--test
str = "A prickly stinging ov*rcom*s your body, f*d*ng aw*y into nubbne*s."


-- process it
str = string.gsub (str, "[$().%%%[%]+%-?\%^]", "%%%1")  -- fix up special characters EXCEPT *
str = string.gsub (str, "*", ".")   -- convert wildcards
str = "^" .. str .. "$"    -- anchor it

print ("str=", str)  -- debug

if string.match ("A prickly stinging overcomes your body, fading away into numbness.", str) then
  Note ("paralysis")
else
  Note("Paralysis Illusion!!")
end


Amended on Sun 01 Feb 2009 04:16 AM by Nick Gammon
Australia Forum Administrator #29
Just to demonstrate what I am talking about. If we use the simple version you had (just converting asterisks to dots), and we are getting illusions from other players, than can easily make it fail. For example (note the square bracket):


str = "A prickly stinging ov*rcom*s your body, [f*d*ng aw*y into nubbne*s."

str = string.gsub (str, "*", ".")   -- convert wildcards
if string.match ("A prickly stinging overcomes your body, fading away into numbness.", str) then
  Note ("paralysis")
else
  Note("Paralysis Illusion!!")
end


Try that and it gives:


Run-time error
World: smaug 2
Immediate execution
[string "Immediate"]:4: malformed pattern (missing ']')
stack traceback:
        [C]: in function 'match'
        [string "Immediate"]:4: in main chunk


See? The bracket which has made its way into the regexp causes your script to fail. It won't take players long to figure that one out.

Whereas my suggestion version is immune to that:


--test
str = "A prickly stinging ov*rcom*s your body, [f*d*ng aw*y into nubbne*s."


-- process it
str = string.gsub (str, "[$().%%%[%]+%-?\%^]", "%%%1")  -- fix up special characters EXCEPT *
str = string.gsub (str, "*", ".")   -- convert wildcards
str = "^" .. str .. "$"    -- anchor it

print ("str=", str)  -- debug

if string.match ("A prickly stinging overcomes your body, fading away into numbness.", str) then
  Note ("paralysis")
else
  Note("Paralysis Illusion!!")
end

Output

str= ^A prickly stinging ov.rcom.s your body, %[f.d.ng aw.y into nubbne.s%.$
Paralysis Illusion!!
Amended on Sun 01 Feb 2009 04:22 AM by Nick Gammon
#30
Excellent.