| 10-16-2009, 12:15 AM | #1 |
GetProc is based on ChanceToProc by Ammorth. It takes the same concept and gives it a much more usable interface as well as some functional improvements. GetProc allows you to smooth out the streakiness of random procs. GetRandomReal() often produces long strings of high or low values, which may result in a relatively rare random proc occuring many times in a row. While using GetProc, each time a proc occurs it becomes less likely, and each time a proc fails to occur it becomes more likely. The overall chance of the proc occuring remains the same, but long streaks of the same result become far less likely. The strength of the steak dampening is configurable using the Weight variable. Many people find it difficult to intuitively grasp the effects of this technique, so here are some hard numbers, based on using GetProc on a 50% chance 1000 times per test and analyzing the results: Format: <streak length>*<number of occurrences> Code:
Weight: 0.0; Streaks: 2*157, 3*54, 4*23, 5*12, 6*13, 7*2, 8*1, 12*1, 13*1 Weight: 0.1; Streaks: 2*143, 3*51, 4*44, 5*12, 6*3 , 7*1, 8*1 Weight: 0.2; Streaks: 2*156, 3*70, 4*31, 5*11 Weight: 0.3; Streaks: 2*158, 3*63, 4*31, 5*8 Weight: 0.4; Streaks: 2*198, 3*64, 4*15 Weight: 0.5; Streaks: 2*203, 3*78, 4*9 Weight: 0.6; Streaks: 2*219, 3*55, 4*9 Weight: 0.7; Streaks: 2*212, 3*51, 4*6 Weight: 0.8; Streaks: 2*253, 3*28 Weight: 0.9; Streaks: 2*236, 3*24 Weight: 1.0; Streaks: 2*256 In addition to looking at the way the weight value prevents streaks, we can analyze the effect it has on gameplay moment-to-moment. Let's say we're using 0.5 weight (default) with a hero who has a 20% critical chance: Code:
Let W = Weight * 0.5 * Min(chance, 1 - chance) Chance lost on true: W / chance Chance gained on false: W / (1 - chance) therefore: W = 0.5 * 0.5 * 0.2 = 0.05 Chance lost on true: 0.05 / 0.2 = 25% Chance gained on false: 0.05 / 0.8 = 6.25% Attack 1: 20.00% chance, false, gain 6.25% Attack 2: 26.25% chance, false, gain 6.25% Attack 3: 32.50% chance, false, gain 6.25% Attack 4: 38.75% chance, true, lose 25% (now very unlikely to crit back-to-back...) Attack 5: 13.75% chance, false, gain 6.25% Attack 6: 20.00% chance, back to start Excessive randomness is disruptive to game balance. Unchecked, the random number generator can determine the outcome of an entire fight, regardless of the skill of players involved. Random skills are more balancable when they behave the way you expect them to behave in the short term as well as the long term. A hero whose DPS is highly affected by critical hits should give a fair fight to every opponent it encounters, rather than obliterating one opponent with 8 critical hits, and being obliterated by the next with 0 critical hits. Using GetProc makes randomness a much more controllable game mechanic. And using static ifs and optional library requirements, it is easy to make GetProc optional for any scripts/systems that involve random events. JASS:library GetProc initializer Init //=========================================================================== // Information: //============== // // GetProc allows you to smooth out the streaky behavior of random procs. // GetRandomReal() often produces long strings of high or low values, which // may result in a relatively rare proc occuring many times in a row. While // using GetProc, each time a proc occurs it becomes less likely, and each // time a proc fails to occur it becomes more likely. The overall chance of // the proc occuring remains the same, but long streaks of the same result // become far less likely. // // By adjusting the Weight value, you can control the extent to which // streaky behavior curtailed. At a low Weight value, it will produce a // series of results that resembles random results, but without the streaky // behavior of WC3's random number generator. At very high weight values, // procs will occur roughly every 1 / ProcChance attempts, although a dec- // ent amount of variation will remain. For most maps, weight values between // 0.25 and 0.75 should be acceptable. // //=========================================================================== // How to use GetProc: //===================== // // Since proc chances vary on a per-event basis, you must specify which // event you want a proc for using a unique integer. The easiest way to get // a unique integer is to declare a key, shown in the example below. (It would // also be possible to use the rawcode of a spell.) // // Procs are typically associated with specific units. For example: you // want to determine whether a particular unit scored a critical hit or not. // First you'd need a unique key representing critical hits. Then you would // call GetProc(key, unit, chance) to determine whether the crit occured: // // globals // key CriticalHit // endglobals // // function OnDamage takes nothing returns nothing // if GetProc(CriticalHit, GetEventDamageSource(), CritChance) then // call DoCriticalHit() // endif // endfunction // // Some procs may not be associated with a specific unit. For example: every // 10 seconds there is a chance to spawn a group of monsters. First, you would need a // declare key for that specific event. Then you would then call GetProc(key, null, // chance) to get a boolean indicating whether the spawn occured: // // globals // key SpawnMonsters // endglobals // // function EveryTenSeconds takes nothing returns nothing // if GetProc(SpawnMonsters, null, SpawnChance) then // call DoSpawnMonsters() // endif // endfunction // //=========================================================================== // GetProc API: //============== // // GetProc(key, unit, chance) -> boolean : // This function requires three parameters and returns a boolean that // indicates whether a particular proc occured or not. The first parameter // must be a unique integer value that represets the proc type. The second // is the particular unit you're getting a proc for; you may use null if // the proc is not associated with a unit. The final parameter is the base // chance value of the proc occuring. // // GetProcChance(key, unit, chance) -> real : // This function takes the same parameters as GetProc, and returns the // percentage chance that the proc would currently occur. // // SetProcWeight(key, weight) : // This function allows you to override the default Weight value for // the proc associated with a particular key. // // ResetProcsForUnit(unit) : // This function resets the percentage chance of all procs associated // with this unit occuring. // // ResetAllProcs() : // This function resets the percentage chance of all procs occuring. // //=========================================================================== // Configuration: //================ globals private constant real Weight = 0.5 //Set this to a value between 0 and 1. At 0, results are not affected //at all. At 1, streaky results are very limited. Leaving the weight //at 0.5 should be a good compromise for most maps. private constant integer MaxProcs = 8190 //You should leave this alone unless you happen to get an error message. //It should be impossible to run out of Proc instances unless you leak //handle references. If you do run out, increase this value as needed. endglobals //=========================================================================== globals private hashtable ht = InitHashtable() private key Weights private key Resets endglobals private struct Proc [MaxProcs] static integer allresets = 0 integer reset = 0 integer allreset = 0 real trues = 0. real falses = 0. real oldchance = 0. endstruct function GetProc takes integer key, unit u, real chance returns boolean local integer id = GetHandleId(u) local real weight = Weight local Proc p if not HaveSavedInteger(ht, key, id) then call SaveInteger(ht, key, id, Proc.create()) endif set p = LoadInteger(ht, key, id) if p == 0 then call BJDebugMsg("GetProc warning: Ran out of Proc instances. Increase MaxProcs in the configuration section.") return false endif if p.allreset < Proc.allresets then set p.allreset = Proc.allresets set p.trues = 0 set p.falses = 0 endif if p.reset < LoadInteger(ht, Resets, id) then set p.reset = LoadInteger(ht, Resets, id) set p.trues = 0 set p.falses = 0 endif if chance != p.oldchance then set p.oldchance = chance set p.trues = 0 set p.falses = 0 endif if chance >= 1. then set p.trues = p.trues + 1 return true elseif chance <= 0. then set p.falses = p.falses + 1 return false endif if HaveSavedReal(ht, Weights, key) then set weight = LoadReal(ht, Weights, key) endif set weight = weight * 0.5 * RMinBJ(chance, 1. - chance) if GetRandomReal(0., 1.) <= chance - (p.trues*weight) / chance + (p.falses*weight) / (1. - chance) then set p.trues = p.trues + 1. return true endif set p.falses = p.falses + 1. return false endfunction function GetProcChance takes integer key, unit u, real chance returns real local Proc p = LoadInteger(ht, key, GetHandleId(u)) local real weight = Weight if p == 0 then return chance elseif p.allreset < Proc.allresets then return chance elseif p.reset < LoadInteger(ht, Resets, GetHandleId(u)) then return chance elseif chance != p.oldchance then return chance elseif chance >= 1. then return 1. elseif chance <= 0. then return 0. endif if HaveSavedReal(ht, Weights, key) then set weight = LoadReal(ht, Weights, key) endif set weight = weight * 0.5 * RMinBJ(chance, 1. - chance) return chance - (p.trues*weight) / chance + (p.falses*weight) / (1. - chance) endfunction function SetProcWeight takes integer key, real weight returns nothing if weight == Weight then call RemoveSavedReal(ht, Weights, key) endif call SaveReal(ht, Weights, key, weight) endfunction function ResetProcsForUnit takes unit u returns nothing call SaveInteger(ht, Resets, GetHandleId(u), LoadInteger(ht, Resets, GetHandleId(u)) + 1) endfunction function ResetAllProcs takes nothing returns nothing set Proc.allresets = Proc.allresets + 1 endfunction //=========================================================================== private function UnitEntersMap takes nothing returns boolean call ResetProcsForUnit(GetFilterUnit()) return false endfunction private function Init takes nothing returns nothing local trigger t = CreateTrigger() local region maparea = CreateRegion() call RegionAddRect(maparea, bj_mapInitialPlayableArea) call TriggerRegisterEnterRegion(t, maparea, Condition(function UnitEntersMap)) endfunction endlibrary |
| 10-16-2009, 12:34 AM | #2 |
Would be useful for DotA && Rikter .. But really, I don't like random in spells anyway .. Could be useful for other game mechanics. Whats max stored in a hashtable btw? I doubt this could ever run out even if you tried -> even dota only uses 1 hashtable for all spells in map (is like lhv ...) |
| 10-16-2009, 02:46 AM | #3 |
I've seen this in use elsewhere and is even already approved, but I've honestly never liked it. Honest to God, as a player, the weight of 0 is the most awesome of the lot. I love having the chance to get 20 crits in a row out of raw randomness. The randomness is part of what makes it crazy fun in the first place. Why do you keep remaking everything everyone else has already made? The only differences between yours and Ammorth's is the API, and honestly, what you're doing is 100% against all movement towards standards. But whatever. |
| 10-16-2009, 03:22 AM | #4 | ||||
Quote:
Quote:
Quote:
Quote:
Furthermore, if a major improvement can be made, a new script can and should be created to replace an old one. |
| 10-16-2009, 04:21 AM | #5 | |||
No, no one uses ChanceToProc because no one cares about weighted percent chances. I am not going to suddenly jump on this idea just because you now use a hashtable. Quote:
Quote:
Quote:
|
| 10-16-2009, 05:31 AM | #6 | |||
Quote:
Quote:
Quote:
Seriously, compare them side by side: JASS:globals key CriticalHit endglobals function OnDamage takes nothing returns nothing if GetHandleProc(CriticalHit, GetEventDamageSource(), CritChance) then call DoCriticalHit() endif endfunction JASS:globals Chance array CriticalHitChances endglobals function OnDamage takes nothing returns nothing local integer id = GetUnitId(GetEventDamageSource()) local Chance c = CriticalHitChances[id] if c == 0 then set c = Chance.create(Weight, 0) set CriticalHitChances[id] = c endif if c.getPercent() != CritChance call c.setPercentage(Weight, CritChance) //Calling this resets the streak dampening, so it must be endif //avoided when possible by comparing to previous chance if c.isProc() then call DoCriticalHit() endif endfunction You have to recode my entire GetHandleProc function inside every place a handle-related proc is used to duplicate the same functionality. This is why I said ChanceToProc's API sucks. |
| 10-16-2009, 07:37 AM | #7 | |
Quote:
By giving them an actual number, no matter how much action has to be done before, it is calculateable and the player gets a know about how good it really is and when its next time casted. To get to an end, I wouldn't use this or the other library, just because randomness isn't a good things at all for players, and even if, I wouldn't want to make it that complicated as you have. GetRandomReal(0., 1.) for example is just enough for randomness, you don't need to weight it. This library is really fine, but I guess, like the Colored Teleports, its nothing I need to have. |
| 10-16-2009, 08:18 AM | #8 | |
Every game ever made that includes random elements is going to disagree with you. There are tons of things that don't work well except as random chances. Thousands of games have % chance to crit. Bonus damage on every Xth attack is an almost unheard of mechanic because of how conceptually unintuitive it is. Besides, it is abusable; you can just use X-1 attacks on random critters to ensure you start the next fight with a crit. Quote:
So clearly, GetRandomReal() is not good enough for you, because the results are too unreliable. Incidentally, using a weighted system solves exactly this problem. Your reasoning is completely self-defeating. |
| 10-16-2009, 08:24 AM | #9 | |||
Quote:
Quote:
Quote:
|
| 10-16-2009, 08:29 AM | #10 |
Right. You don't have a problem with weighted procs, you have a problem with the entire concept of percentage chances being used in video games. I don't think you're the target audience for this script. |
| 10-16-2009, 08:36 AM | #11 | |
Quote:
If I have to stick to an advanced random engine I would maybe need your library, but I think for me it has no use, until now. But maybe its only a matter of time. Maybe you ask why I discuss that much, even if I won't use it, its probably because I like to discuss scripts and to get whats behind that, and to reveal such stuff as bugs and its usefulness. |
| 10-16-2009, 09:24 AM | #12 | |
Quote:
With weighted procs, each successive event is slightly less random, until it actually becomes impossible for the same thing to happen too many times in a row. A high Weight value (0.75+) actually produces really similar results to "bonus damage every X attacks," except that you don't always know when the Xth attack will come up, so that it's not exploitable and maintains the appearance of being percentage based. |
| 10-16-2009, 01:42 PM | #13 | ||
Quote:
And I agree that Ammo's is bloated, but I hate yours even more. If you could do it like his and do all of the logistics internally, then it'd be cool. Seriously, as you were, let's compare them side to side with my suggestion: Yours:globals key CriticalHit endglobals function OnDamage takes nothing returns nothing if GetHandleProc(CriticalHit, GetEventDamageSource(), CritChance) then call DoCriticalHit() endif endfunction His:globals integer array Chances endglobals function OnDamage takes nothing returns nothing if GetProc(Chances[GetUnitId(GetEventDamageSource())], CritChance) then call DoCriticalHit() endif endfunction Quote:
|
| 10-16-2009, 02:16 PM | #14 |
doesnt ammos script need a "chance array" for every proc type aswell ??? whats worse, hundreds of integer arrays, or hundreds of constant integers ? And if something is already approved, which does the same thing, it shouldnt be necessary, that you argue if this script is useful or not. If you really think something like this is not necessary (My opinion clearly is, that it IS useful for some applications) you shouldnt have approved Ammo's script in the first place, but you didnt even mention your thoughts about weighted procs in Ammos thread. This really makes you look biased IMO. One example where i would use a weighted proc is for bosses of RPGs: it really sucks if your whole party gets killed, only because the boss got 5-10 critical strikes in a row... When you balance the healer, you would most likely take the average damage of the boss into account, so the healer of the party wouldnt be able to heal these massive ammounts of damage... Of course you can balance the healer in a way that he CAN heal such crit' streaks, but then he would be too powerful when the boss doesnt crit that often. For use in PVP applications, i think something like weighted procs arent that necessary. Even when i would use it for some special cases, i would choose a relativly low weight. |
| 10-16-2009, 02:56 PM | #15 | ||||
Quote:
Quote:
I'd much, much rather unituserdata. I mean, okay, I guess you can use unituserdata for grim's method too by just passing GetUnitId(MyUnit) as the key, but when that key recycles it has possibility for collisions as well. Hrm, I mean, I guess my way has the same problems. Lame. It's entirely possible that I didn't think this through and grim's actually right about having to use arbitrary integers. (Fuck) The whole key business might actually be better since you could just use the spell's raw id for the integer and not have to use a struct. Well, fine, I agree, maybe this is the better way. Quote:
I clearly understand the purpose of this and that script, I just don't like it on a fundamental level. Quote:
Anyways, whatever, I don't like what this does, but some other random mapmaker might think this is cooler than random numbers and want to use it. Great, fine, I still hate the unique key crap. See above. |
