• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

[Snippet] IPool

Updated!

Script:

JASS:
library IPool requires Table, Alloc
/*
IPool 3.0.0.0 by Bribe

Special thanks to Pyrogasm on wc3c.net for the original Pools resource, and to
Rising_Dusk for popularizing it.

Quick Intro of IPool:

Do you want a random integer from a multiple-choice list instead of a number
between x and y? Do you want one of those choices to have a lower or higher chance
to be picked? How about each item has its own odds of being picked?

IPool is similar to the native types itempool and unitpool, however returns
integers instead of handles. Integer-based pools can be used for all sorts of
things, ranging from randomized creep drops, spawning systems, random abilities
being cast (a la traps), random instances of a atruct, etc.

Object-Types of IPool:

    IPool - Fast .getItem() method, slow add/remove. Uses a Table index formatted
    as an array to get a random item without searching.

    SubPool - Fast add/remove, slower .getItem method. Uses an O(n) search to
    get a random item.

Main IPool API:
    
    IPool.create()->IPool
        returns a new IPool for your needs
    
    iPoolInstance.flush()
        reset all values of the IPool to default settings
    
    iPoolInstance.destroy()
        use this if you are totally done with that IPool

    iPoolInstance.add(integer value, integer weight)
        add any integer to the pool. The weight must be greater than 0 to have a
        chance of being picked.

    iPoolInstance.remove(integer value)
        that value will no longer have a chance if you use this method on it!

    iPoolInstance.getItem()->integer
        returns one of the integers you added. Has higher chance to pick an item
        with higher weight

Main SubPool API:

    SubPool.create(integer totalWeight)->SubPool
        The totalWeight must be equal to or greater than the sum of all items you
        will add to the SubPool. Each added item has a chance of itsWeight/
        totalWeight.

    The rest of the main API is the same as IPool. Only keep in mind that
    subPoolInstance.getItem() will return 0 in many cases due to the improbability
    of an integer being picked.

Secondary API for both structs is included as follows:

    poolInstance.contains(integer value)->boolean
        Was the value added to the pool?

    poolInstance.copy()->pool
        Returns a new pool with the same properties as the pool you want copied.

    debug poolInstance.print()
        Useful for identifying what objects are in the pool during tests.

    poolInstance.lock/unlock()
        SubPools can associate with another subPool and/or an IPool. If you
        have a single nested pool you wish to not get auto-destroyed, simply
        call pool.lock(). pool.unlock() reverses it. If you do not use these, all
        nested pools will auto-destruct when the containing SubPool is destroyed.

API for getting/setting the weight of an already-added value to a pool:

    iPoolInstance.add(integer value, integer additionalWeight)
        You can add more weight to a value in IPool, but the only way to reduce
        it is to completely null it using the .remove(integer value) method. This
        is to avoid the cumbersome giant that was the original IPool.

    subPoolInstance[integer value].weight = integer newWeight
        Setting a SubPool value's weight can be done arbitrarily.

    iPoolInstance.weightOf(integer value)->integer
    &
    subPoolInstance[integer value].weight
        Just in case you lost track of how much weight you assigned to a value.

API for nesting pools:

    subPoolInstance.subPool = SubPool.create()
        The SubPool member can be get and set arbitrarily. It cannot be the same
        SubPool as the SubPool that's trying to nest it, as that would cause
        recursion errors.
        If the SubPool was not able to pick an integer, it will default to the
        next nested SubPool within itself.

    subPoolInstance.pool = IPool.create()
        The .pool member can be get and set arbitrarily. If the SubPool and any
        nested SubPools could not pick an integer, this pool will be the last resort.
*/
private module Init
    private static method onInit takes nothing returns nothing
        set .tar = TableArray[8192]
    endmethod
endmodule
private struct data extends array
    static TableArray tar
    integer int
    integer locks
    integer weight
    implement Alloc
    implement Init
endstruct

    private function Create takes nothing returns data
        local data this = data.allocate()
        set this.locks = 0
        return this
    endfunction
    private function Destroy takes data this returns boolean
        if this.locks == -1 or this == 0 then
            debug call BJDebugMsg("IPool Error: Attempt to double-free instance!")
            return false
        endif
        set this.locks = -1
        call this.deallocate()
        return true
    endfunction

    private function Lock takes data i returns nothing
        if i.locks == -1 then
            debug call BJDebugMsg("IPool Error: Attempt to lock a destroyed instance!")
            return
        endif
        set i.locks = i.locks + 1
    endfunction
    private function Unlock takes data i returns boolean
        if i.locks == -1 then
            debug call BJDebugMsg("IPool Error: Attempt to unlock destroyed instance!")
            return false
        endif
        set i.locks = i.locks - 1
        return i.locks == 0
    endfunction

struct IPool extends array
   
    method operator weight takes nothing returns integer
        return data(this).weight
    endmethod
    private method operator weight= takes integer lbs returns nothing
        set data(this).weight = lbs
    endmethod

    private method operator table takes nothing returns Table
        return data(this).int
    endmethod
    private method operator table= takes Table t returns nothing
        set data(this).int = t
    endmethod

    static method create takes nothing returns thistype
        local thistype this = Create()
        set this.table = Table.create()
        return this
    endmethod
    method flush takes nothing returns nothing
        call this.table.flush()
        call data.tar[this].flush()
        set this.weight = 0
    endmethod

    method destroy takes nothing returns nothing
        if Destroy(this) then
            call this.table.destroy()
            call data.tar[this].flush()
            set this.weight = 0
        endif
    endmethod

    method lock takes nothing returns nothing
        call Lock(this)
    endmethod
    method unlock takes nothing returns nothing
        if Unlock(this) then
            call this.destroy()
        endif
    endmethod

    //One-liner method to get a random item from the pool based on weight
    method getItem takes nothing returns integer
        return this.table[GetRandomInt(0, this.weight -1)]
    endmethod

    method weightOf takes integer value returns integer
        return data.tar[this][value]
    endmethod
    method chanceOf takes integer value returns real //returns between 0. and 1.
        return this.weightOf(value) / (this.weight + 0.) //don't divide by 0 here or else!
    endmethod
    method contains takes integer value returns boolean
        return data.tar[this].has(value)
    endmethod

    method add takes integer value, integer lbs returns nothing
        local Table tb = this.table
        local integer i = this.weight
        if lbs < 1 then
            debug call BJDebugMsg("IPool Error: Tried to add value with invalid weight!")
            return
        endif
        set data.tar[this][value] = data.tar[this][value] + lbs
        set lbs = i + lbs
        set this.weight = lbs //Important
        loop
            exitwhen i == lbs
            set tb[i] = value //treat this.table as an array
            set i = i + 1
        endloop
    endmethod
   
    method remove takes integer value returns nothing
        local Table tb = this.table
        local Table new
        local integer i = this.weight
        local integer n = 0
        local integer val
        if not this.contains(value) then
            debug call BJDebugMsg("IPool Error: Attempt to remove un-added instance!")
            return
        endif
        set new = Table.create()
        set this.table = new
        loop
            set i = i - 1
            set val = tb[i]
            if val != value then
                set new[n] = val //write to the new Table without gaps
                set n = n + 1
            endif
            exitwhen i == 0
        endloop
        set this.weight = n //lower pool weight
        call tb.destroy() //abandon old Table instance
        call data.tar[this].remove(value) //clear the value's weight now that it's gone
    endmethod

    method copy takes nothing returns thistype
        local thistype new = .create()
        local integer i = this.weight
        local Table tt = this.table
        local Table nt = new.table
        local Table dt = data.tar[new]
        local integer val
        if i == 0 then
            debug call BJDebugMsg("IPool Error: Attempt to copy invalid instance!")
            call new.destroy()
            return 0
        endif
        set new.weight = i
        loop
            set i = i - 1
            exitwhen i == 0
            set val = tt[i]
            set nt[i] = val
            set dt[val] = dt[val] + 1
        endloop
        return new
    endmethod
       
    static if DEBUG_MODE then
        method print takes nothing returns nothing //print the array of the pool
            local string s = "IPool: |cffffcc33Weight: "
            local integer i = this.weight
            local Table t = this.table
            set s = s + I2S(i) + "; Indices: "
            loop
                set i = i - 1
                exitwhen i <= 0
                set s = s + "[" + I2S(t[i]) + "]"
            endloop
            call BJDebugMsg(s + "|r")
        endmethod

    endif

endstruct

//New struct to handle deliberately-rare chances
struct SubPool extends array
   
    private IPool iPool //for association if you want it
    private thistype nest //you can nest IPoolMinis for poolception

    //you can change a value's weight via subpoolinstance[value].weight = blah
    //you can also change the entire pool's weight via subpoolinstance.weight = blah.
    method operator weight takes nothing returns integer
        return data(this).weight
    endmethod
    method operator weight= takes integer lbs returns nothing
        set data(this).weight = lbs
    endmethod

    private method operator value takes nothing returns integer
        return data(this).int
    endmethod
    private method operator value= takes integer val returns nothing
        set data(this).int = val
    endmethod

    private thistype next
    private thistype prev

    method operator pool takes nothing returns IPool
        return this.iPool
    endmethod
    method operator pool= takes IPool ip returns nothing
        if ip != 0 then
            call ip.lock()
        endif
        if this.iPool != 0 then
            call this.iPool.unlock()
        endif
        set this.iPool = ip
    endmethod

    static method create takes integer totalWeight returns thistype
        local thistype this = Create()
        set this.next = this
        set this.prev = this //I'm my own best friend
        set this.weight = totalWeight
        return this
    endmethod
    method destroy takes nothing returns nothing
        local thistype curr = this
        if this.next == -1 then
            debug call BJDebugMsg("SubPool Error: Attempt to double-free!")
            return
        endif
        loop
            set curr = curr.next
            call Destroy(curr) //destroy all the things
            exitwhen curr == this
        endloop
        set this.next = -1
        set this.pool = 0
        if this.nest != 0 then
            if data(this.nest).locks == 1 then
                call this.nest.destroy()
            else
                call Unlock(this.nest)
            endif
            set this.nest = 0
        endif
        call data.tar[this].flush()
    endmethod

    method lock takes nothing returns nothing
        call Lock(this)
    endmethod
    method unlock takes nothing returns nothing
        if Unlock(this) then
            call this.destroy()
        endif
    endmethod

    method operator subPool takes nothing returns thistype //need to return thistype and not IPool :P
        return this.nest
    endmethod
    method operator subPool= takes thistype ip returns nothing
        if this == ip then
            debug call BJDebugMsg("SubPool Error: Don't set a subPool within itself. Use the .copy() method, instead.")
            return
        endif
        if ip != 0 then
            call ip.lock()
        endif
        if this.nest != 0 then
            call this.nest.unlock()
        endif
        set this.nest = ip
    endmethod

    method add takes integer val, integer lbs returns nothing
        local thistype new
        if lbs <= 0 then
            debug call BJDebugMsg("SubPool Error: Don't add a value without weight")
            return
        endif
        if data.tar[this].has(val) then
            set new = data.tar[this][val]
            set new.weight = new.weight + lbs
            return
        endif
        set new = Create()
        set new.prev = this.prev
        set this.prev.next = new
        set this.prev = new
        set new.next = this

        set new.value = val
        set new.weight = lbs
       
        set data.tar[this][val] = new
    endmethod

    method contains takes integer val returns boolean
        return data.tar[this].has(val)
    endmethod
    method operator [] takes integer val returns thistype
        return data.tar[this][val]
    endmethod
    method operator []= takes integer val, integer newWeight returns nothing
        set this[val].weight = newWeight
    endmethod

    method remove takes integer val returns nothing
        local thistype node = this[val]
        if Destroy(node) then
            set node.prev.next = node.next
            set node.next.prev = node.prev
            call data.tar[this].remove(val)
        else
            debug call BJDebugMsg("SubPool Error: Attempt to remove non-added value")
        endif
    endmethod
    method getItem takes nothing returns integer
        local thistype curr = this
        local integer i = GetRandomInt(1, this.weight)
        loop
            set curr = curr.next
            set i = i - curr.weight
            exitwhen i <= 0
        endloop
        if curr == this then
            if this.nest != 0 then
                set i = this.nest.getItem()
            else
                set i = 0
            endif
            if i == 0 and this.pool != 0 then //if no low-probability item could be found...
                set i = this.pool.getItem() //pick a random int from main pool
            endif
        else
            set i = curr.value
        endif
        return i
    endmethod

    method copy takes nothing returns thistype
        local thistype new = .create(this.weight)
        local thistype curr = this
        set new.pool = this.iPool
        set new.subPool = this.nest
        loop
            set curr = curr.next
            exitwhen curr == this
            call new.add(curr.value, curr.weight)
        endloop
        return new
    endmethod

    static if DEBUG_MODE then
        method print takes nothing returns nothing
            local thistype curr = this
            local string s = "SubPool: |cffffcc33Instance: " + I2S(this) + ", TotalWeight: " + I2S(this.weight) + ", Indices: "
            if curr.next == this then
                call BJDebugMsg("SubPool is empty!")
                //return
            endif
            loop
                set curr = curr.next
                exitwhen curr == this
                set s = s + "[" + I2S(curr.value) + "]<" + I2S(curr.weight) + ">"
            endloop
            call BJDebugMsg(s + "|r")
            if this.nest != 0 then
                call this.nest.print()
            endif
            if this.iPool != 0 then
                call this.iPool.print()
            endif
        endmethod
    endif
endstruct

endlibrary

JASS:
function TestingIPool takes nothing returns nothing
    local SubPool i = SubPool.create(1000)
    local integer j
    call i.add('Hpal', 1)
    call i.add('hkni', 10)
    call i.add('hpea', 100)
    call i.add('hfoo', 50)
    debug call BJDebugMsg("SubPool GetItem: " + I2S(i.getItem()))
    debug call i.print()
    call i.remove('hpea')
    call i.add('ewsp', 100)
    debug call i.print()
    set i.subPool = SubPool.create(100)
    call i.subPool.add('Amrf', 10)
    call i.subPool.add('Aloc', 10)
    debug call BJDebugMsg("Nested SubPool GetItem: " + I2S(i.subPool.getItem()))
    set i.pool = IPool.create()
    call i.pool.add(9001, 3)
    call i.pool.add(4001, 7)
    call i.pool.add('poop', 10)
    debug call BJDebugMsg("Nested IPool GetItem: " + I2S(i.pool.getItem()))
    debug call i.print()
endfunction

function InitTrig_Pool takes nothing returns nothing
    call TimerStart(CreateTimer(), 0, false, function TestingIPool)
endfunction

Test map attached here: http://www.hiveworkshop.com/forums/2718800-post90.html
 
Last edited:
I am using Table because it has unlimited instances. The way I have it set up you can use this for anything and everything, but it risks hitting the array limit if I didn't use hashtables.

Compare an array data structure to that of a linked list. An array has a size, and each node has a slot that can be used to access it. A linked list has no defined size, and each node is just another link in the chain with no defined slot index. The benefit of using a linked list in normal programming languages is that you can insert/remove from any point in the list with O(1) complexity while maintaining the order of the list without any empty nodes. The benefit to using an array is arbitrary access, so getting a random integer is O(1) but would be O(N) with a linked list.

Internally, this creates its own arrays using Table instances, because normal JASS arrays are in no sense dynamic enough for this kind of operation.
 
Level 6
Joined
Jun 20, 2011
Messages
249
Correct me if i'm wrong, basically what you're doing here is that you're setting the values of an array by range to a respective index?
Such as
Index: 1 Weight: 23
The array values from a[0] -> a[22] will point to "1"
 
Level 6
Joined
Jun 20, 2011
Messages
249
You would be able to avoid hashtable use if you turn it into a module.

This is me being completely hypothetical but how about this? (Using linked list module)
JASS:
local thistype this=base.next
local integer i=0
loop
    set i=i+this.weight
    exitwhen GetRandomInt(0,TOTAL_WEIGHT)<=i
    set this=this.next
endloop
return this
 
Level 6
Joined
Jun 20, 2011
Messages
249
I think you'll have to explain further because i can't see any way to proceed
Also, yes i read RisingDusk's algorithm, and I really don't know what to say, yours is faster when reading values from the pool, but slower when adding to it.
Unless we can think of a better algorithm both versions are viable
 
Hmm that seems a bit overkilled, instead of storing it X times, you could only store it one time and linked the weight to it.
Then a simple random integer will do the job for getting a random "item", and it will still be O(1).

This won't work. It needs to be stored X times. Your method would require it to be O(N) based.

@*Dirac, yes it is an alternative option. I did list that in my documentation (no one ever reads documentation which is why I hesitate to write it much of the time). I think I will make a module similar to what Rising_Dusk made (so that it has as many instances as the user requires).
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
Ok, i was wrong (or at least i can't find something viable yet).
But i still prefer RisingDusk's code though (and usually i don't like his codes).
I even bet than in usual pools the creation of the random "item" will take more time by itself than the random getting.
Now, i realize that his pools are static, but i don't see a need of dynamic, or have you ?

Btw documentation is the most painfull thing in a resource submission :p
 
Level 13
Joined
May 24, 2005
Messages
609
Hi Bribe, in my current project, I've been using this pool system but it's slightly bugged. Though I've managed to fix the bug in the other system, I've come to check out your system as well and I've got a little question concerning the weight: the other system uses real values which felt kina comfortable for adjusting, you are using integers. I guess this decision has been made for a reason, probably in benefit of performance and memory usage? Or just, to provide a system that delivers integers? Maybe one could create two flavors to choose from including integer and real? With the real-based weight, I can easily set a value of 1.0 for standard items and 0.001 or even lower for very rare stuff. To implement the same relation with integers, I need values of 1000 and 1. Now I'm a bit curious because the documentation said that using individual weights higher than a few thousends might break the oplimit.. What if one needs to implement multiple 1:10000 relations?

Can you give me some info which values can be safely used?

Thanks and cheers
 
Last edited:
Level 13
Joined
May 24, 2005
Messages
609
So it's me again. I've done some testing with your system and it does not seem to work properly. I've made a little test code that just adds and changes some unit ids to a pool. The code does not successfully run til the end (the message is not displayed). Instead, it seems to stop execution at the point where the 1660 value is added. My first guess was that the total weight might have reached some kind of a maximum so I put a flush before the critical line but it did not work either.

JASS:
scope IPoolTest initializer Init

private function Init takes nothing returns nothing
    local IPool MyTestPool = IPool.create()
    
    debug call BJDebugMsg("Starting IPool Test..")
    
    call MyTestPool.add('hf00', 1000) 
    call MyTestPool.add('hrif', 660) 
    call MyTestPool.add('hkni', 100)
    call MyTestPool.add('hmtm', 60) 
    call MyTestPool.add('hgyr', 150)
    call MyTestPool.add('hmtm', 80)
    call MyTestPool.add('hspt', 1)
    call MyTestPool.add('hmtm', 100) 
    call MyTestPool.add('hsor', 150) 
    call MyTestPool.add('hdhw', 10) 
    call MyTestPool.add('hmtm', 120)
    call MyTestPool.add('hdhw', 30)  
    call MyTestPool.add('hmpr', 200)
    call MyTestPool.add('hdhw', 50)     
    call MyTestPool.add('ogru', 1) 
    call MyTestPool.add('ohun', 1660) // too much weight beyond this point?
    call MyTestPool.add('otau', 50) 
    call MyTestPool.add('okod', 10) 

    debug call BJDebugMsg("Everything went fine")
    
endfunction

endscope
 
Level 23
Joined
Apr 16, 2012
Messages
4,041
use ExecuteFunc for the whole Init function and write content from Init function to some other function, it should run that way(unless the ExecuteFunc op limit counts to main thread op limit)

I hope this is not too late response
 
Those are some extreme differences in weight. 1660 vs 1? You would probably never see the unit ID with "1" assigned.

I recommend sticking to lower weight values, ranging 1-100, and not all these multiples-of-ten that go into extreme numbers.

However, if you require a high number (in the hundreds or more), I recommend using:

JASS:
call MyTestPool.add('hf00', 1000).evaluate()

As others have said, you are hitting the OP limit. Scope initializers are not started in a new thread. I recommend using "library" instead of "scope".
 
Level 1
Joined
Apr 24, 2009
Messages
44
Going back to a little irrelevant topic but it was mentioned that words such as "item" and "unit" are reserved words. However, since those were used as method names, doesn't jasshelper convert them to something like StructName__item, thus avoiding any compile errors?
 
Level 23
Joined
Apr 16, 2012
Messages
4,041
its the same as Table methods with type names(method unit etc) and yes you are right JH should translate it to ScopePrefix__methodname so it shouldnt matter how you call it
but it depends if he checks for keywords usage before or after name changes
 
Level 17
Joined
Apr 27, 2008
Messages
2,455
Going back to a little irrelevant topic but it was mentioned that words such as "item" and "unit" are reserved words. However, since those were used as method names, doesn't jasshelper convert them to something like StructName__item, thus avoiding any compile errors?

Yep, but the practice to use a reserved keyword is still spaghetti code.
Just because we can, doesn't mean we have to use it.
You can do so much dumb code style with vJass, since it's only a jass preprocessor and can be used abused like a super "textmacro".
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Towards the code. I guess the only thing that matters is:
Can IPool double free a Table instance or not, even if used correctly?
I started reading, but in method shiftWeight I was like aha.. uhm.., mmh yes, what, ok let's start again. :)

Ingame experience:
I've recently changed the drop system which uses IPool from beeing static into a very dynamic version. This means that:
1. The whole item pool is set up after the heroes are picked, based on their unit type.
2. Each item can drop only once per player.
3. Once the item has been droped x times, it is removed from the pool
4. If the pool is empty for a unit type it will be destroyed.

For instance if 3 players have chosen a warrior, 1 has chosen a priest and noone has chosen the archer.
Now no archer items will be inside any IPool this game, each warrior item can be found three times. Each priest item can be found once.

I'm telling this, because it is using the complete API of IPool, which will most likely reveal existing errors/bugs. It will show particularly, whether IPool can double frees a Table from time to time or not.
However I've also added a double free protection into my system. After all shiftWeight is a very large operation and I'm not sure if wacraft waits until the whole operation is finished.
(Consider two units of the same type die simultaneously, rolling the same item which happens to be the last in their pool.)

I've also made two other systems using IPool, which don't use destroy() at all.
A merchant system, where the vendor gets random items into his stock on select event for 60 seconds and/or aslong as he is selected by any player. Works great. Item weights are also seem to work as intended (tested ~50 times). Of course this is a subjective.

A chest system. Works also great.

I'm still working a lot on that map. If I run into issues I'll let you know.
Btw biggest usage of Table instances I've every seen, but w/e Table has (nearly) no limit.
 
Version 2.0.0.2 thanks to BPower:

I just went through and commented the crap out of IPool trying to understand my logic when I was building it.

I have overlooked something. In the "remove" method, when I destroy a Table instance I do not reset the value of this.table[value] back to 0. It is only when removing an item from an IPool instead of removing the IPool together entirely. I have gone ahead and added a hotfix for it, as it would create a double-free of Table if you used this method and then flushed or destroyed the IPool after it.

EIDT to version 2.0.0.1:

Another thing I need to mention upon looking at this further: calling "remove" will also try to do a shiftWeight to 0, which is actually a BJDebug error message. I need to correct the shiftWeight method by implementing the functionality of "remove" into it instead of the BJDebugMsg.

EDIT to version 2.0.0.2: I have just hotfixed which changes a single statement in the shiftWeight method so that it is a less than 0 instead of less than or equal to 0. This should correct all issues. I will test this later on but it logically works perfectly now.
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
You made a mistake somewhere.

I just tested this ingame.
will print out --> IPool Error: Attempt to shift weight of item below zero: ...
JASS:
scope Tester
    globals
        private IPool pool
    endglobals

private struct Test extends array
    private static method test takes nothing returns nothing
        call pool.add('I000', 1)
        call pool.remove('I000')
    endmethod

    private static method onInit takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterPlayerChatEvent(t, Player(0), "test", true)
        call TriggerAddAction(t, function thistype.test)
        set pool = IPool.create()
    endmethod
endstruct
endscope
 
Top