Aller au contenu

Photo

F(x) for weighting loot table


  • Veuillez vous connecter pour répondre
7 réponses à ce sujet

#1
kevL

kevL
  • Members
  • 4 061 messages
Any mathematicians around?

I'm posting this 'cause I think it may be interesting to (a) few; any comments or insights might help me figure this out. Just writing this may help, in fact.


Okay. What I've got so far is a fairly simple loot table (just one table). All that it spawns is gems (yep for crafting). I've tallied up the GP value of one of each gem-type, 24 different gems with total value of 31185 GP; divide the value of a particular gem by 31185 and there's the inverse chance of its drop, out of the sum of all inverses (9653). That's the table.

Now I want to weight the dice-roll  
Random(9563)+1
  with the Level of monster that is going to drop a gem. So I don't have to make multiple tables, this should increase the chance of a high-value gem as monsters get tougher. Yet still retain a possibility for a low-value gem (though at lesser probability), and vice versa!

So, I figure I need a function, and I've got some. Here's where it gets complicated and our eyes glaze over. First I looked into parametric functions, which are two dependent functions (one for x, one for y, related by a common variable t). This will plot on a 2-D graph the gem-type x, the probability of it dropping y, with the Level of the monster as t. Here it is:

x(t)=119.5(t)
y(t)=0.3118(cos(t)^2)

(think, sine wave from 0 to 90 degrees, starts at 1 goes down to 0)

The constants are there to ensure that as t varies from 1 to 80 x varies from 0 to 9562. Further, the probability y varies from 0.3118 (roughly the chance of the lowest-value gem dropping) at  
x=0
 to 0.0094 (roughly the chance of the highest-value gem dropping) at  
x=9562
. I then multiply the Monster-Level by 2.667 to get t, or vice versa (while ensuring there's no divide by zero, and the cosine is capped at less than 90 degrees). Fun .. eh whot.

Ivan Johansen's Graph 4.3

So far so good. But now I figure there are two routes I could take. First, I could just do the die-roll  
Random(9563)+1
 and chuck it into the above stuff somehow; second, I could multiply the two functions together and come up with a third, independent function:

f(x)=(0.3118(cos(2.667(x)))^2)(119.5x)

(think, parabola with pointy part up)

where y is the weight that will affect the die-roll and x is the Monster-Level from 1 to 30+. It is possible to further weight y with the PC-level, simply by adding or subtracting from x.

So this all sounds wonderful, but how do I insert the diceroll into a formula to get the specific gem-drop?

(bonus pt.) If someone's got a tip on how to 'pin' the ends of a curve along the x-axis .. great .. so that when I'm skewing the curve left & right the values on the y-axis remain the same.


p.s. I'm willing to start from scratch if someone has done this before and knows a simpler way.

#2
kevL

kevL
  • Members
  • 4 061 messages
slept on it .. I'm pretty sure one of my mistakes was by trying to modify the result (of above) with the dice-roll. Now I'm attacking the problem by starting out with the die-roll and using it to first modify MonsterLevel, although this effectively caps f(MonstLevel) at MonstLevel .. hm.



working ..

#3
kevL

kevL
  • Members
  • 4 061 messages
this has gone so far over my head I feel like a crewman on the Bounty

*but* .. breakthrough, see that  
^2
? I'm changing that to the inverse of the dice-roll, and it solves two issues: (1) the value of the y-axis at  
x=0
 gets pinned, and (2) the chance of a high-value drop raises and lowers proportional to the die-roll!


*ohm, Heisenberg speak to me now*

Modifié par kevL, 15 décembre 2010 - 09:54 .


#4
kevL

kevL
  • Members
  • 4 061 messages
All Big Werner had to say was, "Go get a coffee."

When I got back I punched some numbers into the formula:

x=(cos(y))^(1/z)

where y is the adjusted Level of the monster, z is
Random(9562)
, and the result x is a biased GemType. Here are some numbers:

              Level-01 Level-02 Level-10 Level-20 Level-30
roll:
1    (0.0001) 1        0.0000   0        0        0
50   (0.0052) 1        0.8119   0        0        0
500  (0.0523) 1        0.9795   0.1774   0.0002   0
1000 (0.1046) 1        0.9899   0.4212   0.0128   0
5000 (0.5229) 1        0.9979   0.8412   0.4180   0.0548
7500 (0.7844) 1        0.9986   0.8911   0.5591   0.1444
9562 (1.0000) 1        0.9989   0.9135   0.6337   0.2191

What this means is that 1st level monsters are gonna drop malachite and 30th level monsters are gonna drop Beljurils and King's Tears. All from one loot table. The zeros above aren't actually zeros; all that's left to do is introduce a suitable factor for x and rewrite my table such that a low x drops a high-value Gem.

Thanks All!   Posted Image

#5
kevL

kevL
  • Members
  • 4 061 messages
hello again,
things didn't turn out to be quite as simple as I'd hoped; you see, the curve tends to lay down along the x-axis as Levels (y) get higher and higher. Hence low rolls (z - high value) were dropping an inordinate amount of Rogue Stones (the highest value gem). So it took a while to introduce a suitable parameter to raise that part of the curve up off the x-axis.

[b]x=(cos(y)^(1/z))+(100z/(y^a))[/b]
where
y is the level of difficulty
z is a random input (dice roll) from 0.0001 to 1.0000
and
x is the weighted output, from 0.0 to 1.0+


Here's the complete script:

// kL_Loot
// by kevL's, 2010 Dec 20
// - rudimentary Treasure system, to spawn in gems for use in
// crafting with TCC (the Complete Craftsman) playing OC + MotB.
// - insert into creatures' OnSpawn.

void CreateLoot(string sLoot)
{
    CreateItemOnObject(sLoot);
}

void main()
{
    // 25% chance of Loot
    if (d4() > 1) return;

    // dice roll, low is good
    int iGemRoll = Random(10000) + 1;
    float fGemRoll = IntToFloat(iGemRoll);
    float fGemModif = fGemRoll / 10000.0f;

    // Character Level (for single-player only)
    int iCharLevel = GetHitDice(GetFirstPC(TRUE));
    // Monster Level
    int iMonstLevel = GetHitDice(OBJECT_SELF);
    // doubling the Monster Level makes it contribute more to the weight
    iMonstLevel *= 2;

    // Effective Level, with some arbitrary adjustments
    int iEffLevel = (iMonstLevel + iCharLevel) / 2;
    // +d20 helps low levels get those special drops
    iEffLevel = iEffLevel + 10 + d20();

    // don't ever let the cosine go below 0 or to 90;
    // values of 12 to 75 preserve moderation
    if (iEffLevel < 12) iEffLevel = 12;
    else if (iEffLevel > 75) iEffLevel = 75;
    float fEffLevel = IntToFloat(iEffLevel);

    // 'Availability' - raising and lowering this adjusts extremes;
    // increasing it means increasing very high-value drops and lowering
    // the lowest-value drops (while maintaining a proper overall weighting),
    // while lowering this value might make the highest-value drops impossible and
    // very low-value drops common (regardless of the cosine angle).
    // It's similar to the cosine angle below, but useful for fine tuning.
    // Think of it as the availability of better items. nb, the end result
    // will never go below zero but a low fAvail will sent the curve above 1.0
    // - adjust in tenths of a point (very sensitive)
    float fAvail = 2.0f;
    // As the cosine angle, y, increases so does the chance of better items;
    // when it's very high, there's no chance of a low-value drop.
    // When it's low, there's still a very slight chance of a better or best item.
    // - a low roll gives a low result and should be interpreted as high value items!
    // (cos(y)^(1/z))+(100z/(y^a))
    float fGemType = (10000.0f * ((pow(cos(fEffLevel), 1.0f / fGemModif)) + (100.0f * fGemModif / (pow(fEffLevel, fAvail)))));
    int iGemType = FloatToInt(fGemType);

    // Loot table of gems, based strictly on GP value out of 9562 total
    string sLoot;
    if (iGemType < 7) sLoot = "cft_gem_13"; // Rogue Stone
    else if (iGemType < 14) sLoot = "cft_gem_12"; // Blue Diamond
    else if (iGemType < 22) sLoot = "cft_gem_15"; // King's Tear
    else if (iGemType < 31) sLoot = "cft_gem_10"; // Star Sapphire
    else if (iGemType < 41) sLoot = "cft_gem_14"; // Beljuril
    else if (iGemType < 51) sLoot = "cft_gem_09"; // Canary Diamond
    else if (iGemType < 69) sLoot = "cft_gem_11"; // Jacinth
    else if (iGemType < 100) sLoot = "nw_it_gem005"; // Diamond
    else if (iGemType < 135) sLoot = "nw_it_gem008"; // Sapphire
    else if (iGemType < 172) sLoot = "nw_it_gem012"; // Emerald
    else if (iGemType < 214) sLoot = "nw_it_gem009"; // Fire Opal
    else if (iGemType < 260) sLoot = "nw_it_gem006"; // Ruby
    else if (iGemType < 312) sLoot = "nw_it_gem010"; // Topaz
    else if (iGemType < 374) sLoot = "nw_it_gem013"; // Alexandrite
    else if (iGemType < 478) sLoot = "nw_it_gem011"; // Garnet
    else if (iGemType < 634) sLoot = "nw_it_gem001"; // Greenstone
    else if (iGemType < 842) sLoot = "nw_it_gem004"; // Phenalope
    else if (iGemType < 1091) sLoot = "nw_it_gem014"; // Aventurine
    else if (iGemType < 1403) sLoot = "nw_it_gem003"; // Amethyst
    else if (iGemType < 2027) sLoot = "nw_it_gem002"; // Fire Agate
    else if (iGemType < 2807) sLoot = "cft_gem_01"; // Bloodstone
    else if (iGemType < 4366) sLoot = "nw_it_gem015"; // Fluorspar
    else if (iGemType < 6445) sLoot = "cft_gem_03"; // Obsidian
    else sLoot = "nw_it_gem007"; // Malachite

    DelayCommand(0.1f, CreateLoot(sLoot));
}

As you can see it takes a 10,000 sided die-roll (z) and weights the result by one-half the result of twice the monster-level plus the character-level (further, adds 10 plus d20) to get the Effective Level y; that's the cosine angle, restricted between 12 and 75 degrees. The variable a, Availability, is there to lift the curve off the x-axis, especially at low z, & changing it will vary the availability of the highest & lowest value drops preferentially. It doesn't so much change the weighting as make it possible or impossible to get extreme results. Sorry, I can't describe Availability well, at all : it's icing on the steak, the seasoning on a cake!! wait a sec ..

If you limit the high angle on the cosine, it limits how much the table gets weighted towards the better drops (lowers the maximum value of the weighting). And when you increase the availability it increases the availability of high value drops, without significantly changing the weighting of the table (decreasing availability decreases the possibility of high value drops). Notice well, however, that this whole system is predicated on the notion that a low roll gives a high value item, and vice versa. Lowering the minimum cosine below, say, 10 puts the roll on a really lousy curve, while 90 would mean you always get the highest valued drop.

Using this system you could theoretically use a drop table with equal probability for all items and simply adjust the function in order to keep the value of your drops proportional to monster and character levels (although I don't recommend it). A good method to test tables and weightings is to use a crate and put your script into the OnOpen event, making necessary changes like assigning an integer or random integer for monster & PC levels. A conditional loop will fill the crate quickly, to see the distribution of items.

So far the script works sweet, with low levels getting lots of malachite, and higher levels getting a more even distribution of gems throughout. Further tweaking has to be done with actual playing (when I next run through the OC + MotB). The goal is to have to make decisions on what to craft with a well-rounded TCC, which I'm modifying and expanding to +8 items (etc) for 20+ level characters. Later I'll be working OC Essences into this script as well.

If you'd like to use this for your module or adapt it for your PW, hey great! Feel free to change the table to your own custom drops or further modify the function. I'm not a stickler for credit (Lord knows we've all had stuff stolen by the wind ..)

p.s. Thanks to Ivan Johansen's Graph 4.3

As a very final note I'd just like to say that that table is the screwiest thing I've ever worked with; the odds of the highest to lowest item (gem) are about 1:1500. The beauty of it is, if the function can be made to work with disparities like that, Let's Party!

#6
NWN DM

NWN DM
  • Members
  • 1 126 messages
My brain hurts.

#7
Olblach

Olblach
  • Members
  • 175 messages
Usually you go with multiple tables. Roll 1d100 and if you get 90+ you go to the higher table.



100x100=10000 ^^

#8
kevL

kevL
  • Members
  • 4 061 messages

NWN DM wrote...

My brain hurts.

how do you think mine felt?
But seriously, I've tried to do stuff this complicated before and know from experience that it's like bashing my head against a 10' thick brick wall .. so, I tried an experiment by posting progress here, and found it was a great vent, the solution coming to me with much less pain & frustration than normal. Hence, my thanks.

Olblach wrote...

Usually you go with multiple tables.


yes, but as you're probably aware that's exactly what I was trying to get away from .. at least on the LootTable end of things. One table, assign probabilities based on whatever level-setting you want, and the function will adjust for whatever you want. For example, the input doesn't have to be based on the level of monster or NPC; a designer could fabricate an area and just call it 'level 10', and if a level 5 Rogue slips in and loots a chest, that can call a script that weights the treasure at level 10 regardless.

If, on the other hand, you're referring to the input roll, it was rather coincidental it's d(10000). It simply fit with the table I'd worked out for gems. Fortunately, though, it's not hard to work with since its still factors of 10. And there's no reason further customization couldn't use d(100) or whatever. I like the idea of a continuous function rather than multiple tables.

note I haven't tested this thoroughly yet; but on the graph and in simple tests it looks good. Plus, when the solution appeared I felt that light go on that hasn't let down in the past

anyway, on to other things