HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

Strings RollTable

10-23-2008, 02:42 AM#1
ProFeT
This is a library taken from my Jass Framework I use for my map project Nights of Kalimdor.


DESCRIPTION
This system is a string parser, that enable us to create very easily "tables" to roll "things".
These "things" could be war3's items (for a looting system), or just numbers (integers, reals) or any other data that could be retrieved from a string.


THOUGHTS
The (theorical) main problem that could be encountered is the (big ?) amount of strings generated by the parsing system, but I don't really know how bad it could be (if someone has answers I miss..)

In the past, I made a very simple roll system based on Blizzard's one, but it couldn't be able to manage priorities or conditional behaviours between table's items so I decided to try using strings.

Even it appears to be harmful for the game's integrity due to the amount of created strings, I hope you'll find it enough interesting to give a feedback and improvement ideas ;)


CODE
Collapse JASS:
library FW
//Extract a string from an other string.
//Example:      FW_SubString( "Hello World", 1, 4 )         //return "ello"
//
    public function Substring takes string s, integer start, integer length returns string
        if( start<0 )then
            set start = 0
        endif
        return SubString( s, start, start+length )
    endfunction

//Find a needle in a string and return the starting position of the first occurence.
//Return -1 if the needle is not found.
//Example:      FW_FindString( "Hello World", "Wo" )         // return 6
//
    public function FindString takes integer start, string s, string needle, boolean caseSensitive returns integer
    local integer i = start
    local integer L = StringLength(needle)
        if( s!="" and needle!="" and (StringLength(needle)<=StringLength(s)) )then
            if( not caseSensitive )then
                set s = StringCase(s,false)
                set needle = StringCase(needle,false)
            endif
            if( i<0 )then
                set i=0
            endif
            loop
                exitwhen( i>StringLength(s) )
                if( SubString(s,i,i+L)==needle )then
                    return i
                endif
                set i=i+1
            endloop
        endif
        return -1
    endfunction


//Find and replace all occurences of a needle in a string.
    public function StringReplace takes string s, string needle, string replacedMsg, boolean caseSensitive returns string
    local integer f
        loop
            set f = FindString( 0, s, needle, caseSensitive )
            exitwhen( f==-1 )
            set s = Substring(s,0,f) + replacedMsg + Substring(s,f+StringLength(needle),-1)
        endloop
        return s
    endfunction

//Removes all spaces or linebreaks from a string.
public function ClearSpaces takes string s returns string
    set s = StringReplace( s, " ", "", false )
    set s = StringReplace( s, "|n", "", false )
    return s
endfunction



scope FWRollTable
//===========================================================================
//CUSTOM:
    private function definedRollTables takes string name returns string
        if( name=="testtable" )then
            return ""
        else
            
        endif
        return ""
    endfunction
//===========================================================================
//STRUCTURE'S DEFINITION:
    private struct s_rolltable
        private string table
        private integer pos = 0
        private real array chances[100]
        private string array values[100]
        private integer count = 0
        private string  dataReadValue   = ""
        private real    dataReadChance  = 100.
        private boolean dataQueued      = false
        //***********************************************************
        //Reading methods
            private method readRandom takes nothing returns string
            local string char = SubString( .table, .pos, .pos+1 )
            local boolean A = false
            local boolean isReal = false
            local integer recordPos
            local integer aI = 0
            local integer bI = 0
            local real aR = 0
            local real bR = 0
            local string temp
                if( char=="(" )then
                    set recordPos = .pos + 1
                    loop
                        set .pos = .pos + 1
                        set char = SubString( .table, .pos, .pos+1 )
                        exitwhen( char==")" )
                        if( char==null )then
                            return "" //missing closing parenthesis
                        elseif( char==";" )then
                            if( not A )then
                                set A = true
                                //read a-value
                                set temp = SubString(.table,recordPos,.pos)
                                set isReal = (FW_FindString( 0, temp, ".", true )>-1)
                                if( isReal )then
                                    set aR = S2R( temp )
                                else
                                    set aI = S2I( temp )
                                endif
                                set recordPos = .pos + 1
                            else
                                return "" //wrong random definition
                            endif
                        endif
                    endloop
                    //Read 2nd value
                    set temp = SubString(.table,recordPos,.pos)
                    if( not isReal )then
                        if( FW_FindString(0,temp,".",true)>-1 )then
                            set isReal = true
                            set aR = I2R(aI)
                            set bR = S2R( temp )
                        else
                            set bI = S2I( temp )
                        endif
                    else
                        set bR = S2R( temp )
                    endif
                    //return random value
                    set .pos = .pos + 1
                    if( isReal )then
                        return R2S( GetRandomReal(aR,bR) )
                    else
                        return I2S( GetRandomInt(aI,bI) )
                    endif
                endif
                return ""
            endmethod
            private method readValue takes nothing returns string
            local string char = SubString( .table, .pos, .pos+1 )
            local integer recordPos = .pos
            local boolean inBracket = (char=="[")
            local string r = ""
                loop
                    //call BJDebugMsg( "  value char= "+ char)
                    if( char=="(" )then
                        if( recordPos<.pos )then
                            set r = r + SubString( .table, recordPos, .pos ) +.readRandom()
                        else
                            set r = r + .readRandom()
                        endif
                        set recordPos = .pos//update record position
                    elseif( char=="{" )then
                        if( recordPos<.pos )then
                            set r = r + SubString( .table, recordPos, .pos ) + .readTable()
                        else
                            set r = r + .readTable()
                        endif
                        set recordPos = .pos//update record position
                    elseif( char=="#" )then
                        if( recordPos<.pos )then
                            set r = r + SubString( .table, recordPos, .pos )
                        endif
                        set .pos = .pos + 1
                        set r = r + .readTableNamed(.readValue())
                        set recordPos = .pos//update record position
                    endif
                    set char = SubString( .table, .pos, .pos+1 )
                    exitwhen( char==":" or char=="&" or (char=="," and not inBracket) or (char=="]" and not inBracket) or char==null )
                    set .pos = .pos + 1
                    set char = SubString( .table, .pos, .pos+1 )
                endloop
                if( recordPos<.pos )then
                    set r = r + SubString( .table, recordPos, .pos )
                endif
                return r
            endmethod
            private method readTableNamed takes string name returns string
            local s_rolltable rt
            local string r = ""
                set name = definedRollTables(name)
                if( name!="" )then
                    set rt = s_rolltable.create( name )
                    set r = rt.Process()
                    call rt.destroy()
                endif
                return r
            endmethod
            private method readTable takes nothing returns string
            local integer start = .pos + 1
            local integer end = -1
            local integer c = 0
            local string r
            local s_rolltable rt
                //Find the end of the table ("}"char)
                    loop
                        set .pos = .pos + 1
                        set r = SubString( .table, .pos, .pos+1 )
                        exitwhen( r==null )
                        if( r=="{" )then
                            set c = c + 1
                        elseif( r=="}" )then
                            if( c==0 )then
                                set end = .pos
                                exitwhen(true)
                            else
                                set c = c - 1
                            endif
                        endif
                    endloop
                //Check
                    if( r==null )then
                        set .pos = end
                        return ""
                    endif
                //Process table
                    set rt = s_rolltable.create( SubString(.table,start,end) )
                    set r = rt.Process()
                //Clean struct and return result
                    set .pos = end + 1
                    call rt.destroy()
                    return r
            endmethod
        
        //***********************************************************
        //Main
            private method saveRead takes boolean increase returns boolean
            local boolean result = false
                if( .dataReadValue!="" )then
                    if( .dataQueued )then
                        //call BJDebugMsg( ":: Random queued '"+.dataReadValue+"', chance= "+R2S(.dataReadChance) )
                        if( FW_MathRoll(.dataReadChance) )then
                            if( .values[.count]=="" )then
                                //call BJDebugMsg( "  ->added (new)" )
                                set .values[.count] = .dataReadValue
                                set .chances[.count] = 100.
                            else
                                //call BJDebugMsg( "  ->added (update)" )
                                set .values[.count] = .values[.count] + "&" + .dataReadValue
                            endif
                            set result = true
                        else
                            //call BJDebugMsg( "  ->failed" )
                        endif
                    else
                        set .values[.count] = .dataReadValue
                        set .chances[.count] = .dataReadChance
                        set result = true
                    endif
                    if( increase and .values[.count]!="" )then
                        //call BJDebugMsg( " ## INCREASE ##" )
                        set .count = .count + 1
                        set .values[.count] = ""
                        set .chances[.count] = 100.
                    endif
                endif
                //call BJDebugMsg( "|cffffcc00stored: "+.values[.count]+"|r" )
                set .dataReadValue = ""
                set .dataReadChance = 100.
                return result
            endmethod
            method Process takes nothing returns string
            local integer i = 0
            local real sum = 0
            local string char
            local real rand
            //Parse table
                loop
                    set char = SubString( .table, .pos, .pos+1 )
                    if( char==null or char=="" )then
                        call .saveRead(true)
                        set .dataQueued = false
                        exitwhen( true )
                    elseif( char=="," )then
                        call .saveRead(true)
                        set .dataQueued = false
                        set .pos = .pos + 1
                    elseif( char=="&" )then
                        set .dataQueued = true
                        call .saveRead(false)
                        set .pos = .pos + 1
                    elseif( char=="#" )then
                        set .pos = .pos + 1
                        set .dataReadValue = .readTableNamed(.readValue())
                    elseif( char=="{" )then
                        set .dataReadValue = .readTable()
                    elseif( char==":" )then
                        set .pos = .pos+1
                        set .dataReadChance = S2R(.readValue())
                    else
                        set .dataReadValue = .readValue()
                    endif
                endloop
            //Roll results
                if( .count>0 )then
                    set rand = GetRandomReal( 0.001, 100. )
                    //call BJDebugMsg( "===========================" )
                    //call BJDebugMsg( " rand: "+R2S(rand) )
                    loop
                        exitwhen( i==.count )
                        //call BJDebugMsg( " value #"+I2S(i)+" : "+.values[i]+"  chance: "+R2S(RMinBJ(100.-sum,.chances[i]))+"%" )
                        set sum = sum + .chances[i]
                        if( rand<=sum )then
                            //call BJDebugMsg( "RESULT : "+.values[i] )
                            return .values[i]
                        endif
                        set i = i + 1
                    endloop
                endif
                return ""
            endmethod
        
        //***********************************************************
        //Constructor
            static method create takes string table returns s_rolltable
            local s_rolltable this = s_rolltable.allocate()
                set this.table = FW_ClearSpaces( table )
                set this.values[0] = ""
                set this.chances[0] = 100.
                return this
            endmethod
    endstruct

//===========================================================================
//Process a "roll table" and return the result as a string.
    function FW_RollTable takes string tableDef returns string
    local s_rolltable rt
    local string r = ""
        if( tableDef!="" )then
            set rt = s_rolltable.create( tableDef )
            set r = rt.Process()
            call rt.destroy()
        endif
        return r
    endfunction

endscope


endlibrary





EXPLANATIONS
Note: The result of a processed table is a string, that might be parsed to be "translated" by your own systems into "game's actions".
Using it is very simple once you understood the simple syntax, then it'll be pretty easy for you to build complex roll tables.
You can process a table with a single function function FW_RollTable takes string tableDef returns string


Structure of <tableDef>
A simple table definition is a string that matches the following structure :
<item1> : <chance1> , <item2> : <chance2> , ...

Note1: <chance> values are in percent.
Example: A:20 //A has 20% chance to be chosen.

Note2: total of "chance" values in a given table is automaticaly caped to 100% (so if the first item has 80% chance to be chosen, giving the second one 60% chance, only make it rolled 20% of times).

Note3: <chance> part is optional, if ommitted the default chance of associated item is 100%.
Example: A:20 , B //B has 80% chance to be chosen (20% + 100% = 120%, caped to 100%)

Note4: Spaces have no meaning in the table definition, so following statements are equivalent : A:20,B A : 20 , B

Custom/Composed return value
If you need to return a custom string containing a syntax character (like ,) just enclose it with [ ].
Example with a war3's item definition that could be used in a looting system [rawcode,charges] : [pnvu,1]:50 , [pnvu,2]:50 (means: 50% chance to get an Invulnerability potion with 1 charge, and also 50% with 2 charges.)

Random definitions
The previous example can be simplified with the use of a random definition, the syntax is ( minValue ; maxValue ) .
Example: [pnvu,(1;2)]

You can get a random real if at least one of both values is specified as a real.
Example: (1.05;2) //might returns "1.785"

Inner tables
Sometime you might want to use kinds of "random definition" but with unequipotential chances to be picked, the simplest way to do that is to use an other rolltable INSIDE the current table !

A simple example is worth words:
[pnvu,{1:60,2:40}] (means: 60% chance to get an Invulnerability potion with 1 charge, 40% with 2 charges.)

Obviously, tables can be nested with no depth limitation.

Results concatenation
You may need to return more than one item from a single table, for example if you are working on a looting system, the roll can return multiple item codes.
To do that, just replace the separating "," between two items by a "&".
Example: [pnvu,1]&[rwiz,0] //always returns an Invulnerability Potion (1 charge) and a Sobi Mask.

You still can define a chance for each item :
Example: [pnvu,1]:50&[rwiz,0]:50
Possible results of previous example are:
25% "[pnvu,1]"
25% "[rwiz,0]"
25% "[pnvu,1]&[rwiz,0]"
25% ""


External tables
Different roll tables might share a common part of data, so instead of repeating it as many time as needed, you can include an external string defined in the "definedRollTables" function.

Example:
Collapse Configuration function:
private function definedRollTables takes string name returns string
    if( name=="WizardLoot" )then
        return "[rwiz]:25,[ciri]:25,[pmna]:25,[mnst]:25"
    endif
    return ""
endfunction
A call of FW_RollTable( "#WizardLoot:50 , [pgma,1]" ) will return 50% of times, one of the four items of the WizardLoot string, else, a Mana Potion (1 charge).




I hope that I don't made any mistake, and wait for your opinion about this system.
10-23-2008, 03:09 AM#2
Vexorian
Quote:
scope FW
utter non-sense...
10-23-2008, 10:53 AM#3
ProFeT
In fact, the FW scope is used for a large group of functions/structures and not only these 3, but in order to keep the prefix of these functions i also kept this scope ;)
10-23-2008, 04:41 PM#4
Vexorian
scope takes your stuff back to the dark age.

You have functions that are supposed to be called from another place... use a library man.
10-23-2008, 05:41 PM#5
ProFeT
You right, the real structure of my data is :
library FW
scope FWRollTable
endscope
endlibrary

I'll modify my first post right now. Any idea about how bad would be the strings generation for a map?
10-28-2008, 08:54 PM#6
ProFeT
Does my question deserve any answer or is this a lame question.. or ?