SplitSpeedwalk (a function done in LPEG)

Posted by WillFa on Sat 27 Jun 2009 08:04 AM — 9 posts, 36,543 views.

USA #0
So you have a super-long, nifty speedwalk that you included comments in. You want to be able to get at your comments in your Lua script.

GetMapperItem can retrieve your comments for you, but there's no SetMappingString. So you can't load your saved speedwalk into the Mapper.

EvaluateSpeedwalk strips the comments out.

What can ya do?


function SplitSpeedwalk(speedwalk)
    lpeg.locale(lpeg)
    local P, C, Cs, V, Ct, Cg, S, Cc = lpeg.P, lpeg.C, lpeg.Cs, lpeg.V, lpeg.Ct, lpeg.Cg, lpeg.S, lpeg.Cc
    local digit, space = lpeg.digit, lpeg.space
    
    local function ExpandCounter (repeated, cmd) 
        local tmp = {} 
        repeated = math.max(1,tonumber(repeated))
        for i = 1,repeated do 
            tmp[#tmp+1] = cmd 
        end 
        return unpack(tmp) 
    end 
    
    local function er (c, i, lvl)
        error(string.format("Bad Speedwalk character near '%s' at position %d", c:sub(i, i), i) , 4 )
    end
    
    local SWdirs = {n = "north", s = "south", e = "east", w = "west", 
                    u = "up", d = "down", f= GetInfo(19),                 }
    local Macros = {L = "lock ", O = "open ", C = "close ", K = "unlock ",} 
    
    local tmp = {}
    for k,_ in pairs(SWdirs) do 
        SWdirs[k:upper()] = SWdirs[k] 
        tmp[#tmp+1] = k
        tmp[#tmp+1] = k:upper()
    end
    local DirChars = table.concat(tmp)
    tmp = nil
    
    local function SWmacros (x,y)
        return Macros[x] .. y 
    end
    
    local
    SpeedWalk = P{"grammar",
                grammar = Ct( ( V"comment"    +                 -- Capture to a table: {a comment} or
                                V"action"     +                 -- an action like LW or
                                V"repeatable" +                 -- a dir that might have a #: 2w 3(jump) e  or
                                space^1       )^0)              -- ignore white space, 0 or more captures. 
                             * (-1 + P(er)),                    -- match to end of string or error.
                comment = C(P"{" * (1-P"}")^1 * P"}"),          -- PCRE: (\{[^\}]+\})
                counter = C(digit^1) + Cc(1) ,                  -- match a number or default to 1 
                special = P"(" * C( (1- P")" )^1) * P")",       -- PCRE: \(([^\)]+)\)
                action  = Cg( C(S"LOCK") * V"dir")/SWmacros,    -- string.gsub(s, "([LOCK])([nsewudfNSEWUDF])", SWMacros) -- kinda
                dir     = Cs(S(DirChars) / SWdirs),             -- string.gsub(s, "([nsewudfNSEWUDF])", SWdirs)
                repeatable = Cg( V"counter" * space^0 *         -- similar to string.gsub(s, "(%d+)%s*([nse...])", ExpandCounter)
                               ( V"special" + V"dir" )) / ExpandCounter, -- but V"counter" is a lot more complex than (%d) :)
                }
    return SpeedWalk:match(speedwalk)
end


This returns a table of the evaluated speedwalk, with comments still included. It is meant to replicate the behavior of GetMappingItem, not Evaluate Speedwalk. Speedwalks with special directions (forward/backward) will be parsed as "forward/backward", not just "forward".

**This has been modified after Erendir's post. I don't see a reason to save bad code for posterity. :)
Amended on Fri 09 Oct 2009 05:53 PM by WillFa
Germany #1
Some fixes:

local P, C, Cs, V, Ct, Cg, S = lpeg.P, lpeg.C, lpeg.Cs, lpeg.V, lpeg.Ct, lpeg.Cg, lpeg.S

and with grammar like this:

grammar = Ct( ( ( (V"counter")^-1 * ( V"special" + V"swmacro" + V"dir"  )
                  + V"comment" + lpeg.digit^1*P(er) )/ EvalCounter
                  + lpeg.space^1)^1) * (-1 + P(er)),

it'll be not more possibly to multiply comments ;)
Amended on Sat 27 Jun 2009 05:49 PM by Erendir
USA #2
Good change!

Although, EvaluateSpeedwalk("4 s") is syntacticly valid - though it does look dumb.

I'll edit the original post with the fixes and changes.
Australia Forum Administrator #3
Looks like a fabulous idea! I love to see LPEG being used in new ways. I must admit when I added LPEG I didn't envisage all these novel ways it could help.

Let's test:


print (SplitSpeedwalk("5N 4W (ne/sw) (say open sesame/kick door) 5U 4(se/nw)"))  --> nil


Oops.

I got that example from the EvaluateSpeedwalk help page.

It looks like the upper-case directions are throwing it out. EvaluateSpeedwalk accepts the upper-case directions like N, S, E, W.

The help text spells it out:


Directions

The directions recognised are: N:north, S:south, E:east, W:west, U:up, D:down.


Interestingly, it accepts LW (lock west) but not W on its own.

You might also want to build in support for the filler (f) character.
Amended on Sat 27 Jun 2009 10:20 PM by Nick Gammon
USA #4
Nice catch Nick.


function SplitSpeedwalk(speedwalk)
    lpeg.locale(lpeg)
    local P, C, Cs, V, Ct, Cg, S = lpeg.P, lpeg.C, lpeg.Cs, lpeg.V, lpeg.Ct, lpeg.Cg, lpeg.S
    local repeated = 1
    local function SWcounter(x) repeated = math.max(1,math.min(x,99)) end
    local function EvalCounter (cmd) 
        local tmp = {} 
        for i = 1,repeated do 
            tmp[#tmp+1] = cmd 
        end 
        repeated = 1
        return unpack(tmp) 
    end 
    local function er (c, i) 
        error(string.format("Bad Speedwalk character near '%s' at position %d", c:sub(i, i), i) )
    end
    local SWdirs = {n = "north", s = "south", 
                    w = "west", e = "east", 
                    u = "up", d = "down", 
                    f= GetInfo(19),
                    }
    local tmp = {}                    
    for k,_ in pairs(SWdirs) do 
        SWdirs[k:upper()] = SWdirs[k] 
        tmp[#tmp+1] = k
        tmp[#tmp+1] = k:upper()
    end
    local DirChars = table.concat(tmp)
    tmp = nil
    
    local function SWmacros (x,y)
        local tmp= {L = "lock ", O = "open ", C = "close ", K = "unlock ",} 
        if tmp[x] and SWdirs[y:lower()] then
            return tmp[x] .. SWdirs[y:lower()] 
        else
            error("Invalid Speedwalk Macro.")
        end
    end
    
    local
    SpeedWalk = P{"grammar",
                grammar = Ct( ( V"comment" + 
                              (V"counter"^-1 * lpeg.space^0 * V"repeatable") +
                              (lpeg.space^1 )
                            )^1) * (-1 + P(er)),
                comment = C(P"{" * (1-P"}")^1 * P"}"),
                counter = C(lpeg.digit^1) /SWcounter,
                repeatable = ( V"special" + V"swmacro" + V"dir"  ) / EvalCounter,
                special = P"(" * C((1-P")")^1)* P")",
                swmacro = Cg( C(S"LOCK") * C(1))/SWmacros,
                dir = Cs(S(DirChars) / SWdirs),
                }

    return SpeedWalk:match(speedwalk) or er(speedwalk,1)
end


Now parses

/tprint(SplitSpeedwalk([[3e 2 S 
       {Foo} 2(jump/fall)3LW ]]))

Correctly with:

1="east"
2="east"
3="east"
4="south"
5="south"
6="{Foo}"
7="jump/fall"
8="jump/fall"
9="lock west"
10="lock west"
11="lock west"


Just like GetMappingItem, this is meant to return "fwd/back" directions; the "jump/fall" isn't a bug.

Amended on Sat 27 Jun 2009 11:48 PM by WillFa
Germany #5
a small "optimization":

                grammar = Ct( ( V"comment" + 
                              (V"counter"^-1 * lpeg.space^0 * V"repeatable") +
                              (lpeg.space^1 )
                            )^0) * (-1 + P(er)),


makes

return SpeedWalk:match(speedwalk) or er(speedwalk,1)

obsolete.
USA #6
Good to know. I was expecting that I'd get a compiler error that the "Grammar could match on empty string".

Strangely enough, I can't seem to make a grammar that causes the error now. I've gotten them at times in the past.


(First post has current code again.)
Amended on Sun 28 Jun 2009 06:42 AM by WillFa
Germany #7
>error that the "Grammar could match on empty string".
it's possible with lpeg.P(-1):match("") - no error
What You mean is the "loop body may accept empty string"-error, like
err = (lpeg.P(1)^0)^1
err:match("")
USA #8
I did not like the SWcounter function, and realized I could do it in lpeg. I got rid of 1 function and an upvalue by tweaking the grammar.

I also realized that EvalSW doesn't like things like "2LW".

I found a quirk in EvalSW; it silently changes "0w" to "w" or "1w". So that's emulated.

The error function has a level specified high enough to point to what called SplitSpeedwalk.

There's one new quirk in this function, "Lf" (Lock fillercommand) is valid syntax, though still a dumb thing for a user to do, like "0w", so I ain't fixin' it. :p

And now with commenty goodness of the lpeg for people that are curious what this gobbledygook means!


function SplitSpeedwalk(speedwalk)
    lpeg.locale(lpeg)
    local P, C, Cs, V, Ct, Cg, S, Cc = lpeg.P, lpeg.C, lpeg.Cs, lpeg.V, lpeg.Ct, lpeg.Cg, lpeg.S, lpeg.Cc
    local digit, space = lpeg.digit, lpeg.space
    
    local function ExpandCounter (repeated, cmd) 
        local tmp = {} 
        repeated = math.max(1,tonumber(repeated))
        for i = 1,repeated do 
            tmp[#tmp+1] = cmd 
        end 
        return unpack(tmp) 
    end 
    
    local function er (c, i, lvl)
        error(string.format("Bad Speedwalk character near '%s' at position %d", c:sub(i, i), i) , 4 )
    end
    
    local SWdirs = {n = "north", s = "south", e = "east", w = "west", 
                    u = "up", d = "down", f= GetInfo(19),                 }
    local Macros = {L = "lock ", O = "open ", C = "close ", K = "unlock ",} 
    
    local tmp = {}
    for k,_ in pairs(SWdirs) do 
        SWdirs[k:upper()] = SWdirs[k] 
        tmp[#tmp+1] = k
        tmp[#tmp+1] = k:upper()
    end
    local DirChars = table.concat(tmp)
    tmp = nil
    
    local function SWmacros (x,y)
        return Macros[x] .. y 
    end
    
    local
    SpeedWalk = P{"grammar",
                grammar = Ct( ( V"comment"    +                 -- Capture to a table: {a comment} or
                                V"action"     +                 -- an action like LW or
                                V"repeatable" +                 -- a dir that might have a #: 2w 3(jump) e  or
                                space^1       )^0)              -- ignore white space, 0 or more captures. 
                             * (-1 + P(er)),                    -- match to end of string or error.
                comment = C(P"{" * (1-P"}")^1 * P"}"),          -- PCRE: (\{[^\}]+\})
                counter = C(digit^1) + Cc(1) ,                  -- match a number or default to 1 
                special = P"(" * C( (1- P")" )^1) * P")",       -- PCRE: \(([^\)]+)\)
                action  = Cg( C(S"LOCK") * V"dir")/SWmacros,    -- string.gsub(s, "([LOCK])([nsewudfNSEWUDF])", SWMacros) -- kinda
                dir     = Cs(S(DirChars) / SWdirs),             -- string.gsub(s, "([nsewudfNSEWUDF])", SWdirs)
                repeatable = Cg( V"counter" * space^0 *         -- similar to string.gsub(s, "(%d+)%s*([nse...])", ExpandCounter)
                               ( V"special" + V"dir" )) / ExpandCounter, -- but V"counter" is a lot more complex than (%d) :)
                }
    return SpeedWalk:match(speedwalk)
end


p.s. this is why I take so long to code a plugin. I end up writing the same code 13 times. :\