ExcellentSP
ExcellentSP

Reputation: 1619

How to distribute values across an array according to percentages

I'm making a DBZ Xenoverse 2 stat distribution guide according to build so you receive consistent gameplay in your attributes. The thought is that if by level 80 you have a total of 332 stat/attribute points to distribute, and you want to keep a consistent point allocation while creating your build, you would need to gather the percentages of the attributes from the total points gathered and assign them according to those percentages for each level. So if I wanted a character build that looked like:

Health - 30
Ki - 42
Stamina - 42
Basic Attacks - 93
Strike Supers - 0 
Ki Blast Supers - 125
Total - 332

The percentages would look like:

Health - 9.03614457831325%
Ki - 12.65060240963855%
Stamina - 12.65060240963855%
Basic Attacks - 28.01204819277108%
Strike Supers - 0%
Ki Blast Supers -37.65060240963855%

So on level 2 (since you don't get stats for level one) you get two stat points and your Attributes would look like this:

Health - 0
Ki - 0
Stamina - 0
Basic Attacks - 1
Strike Supers - 0
Ki Blast Supers - 1

Whereas on level 20 your stats would look like this:

Health - 5
Ki - 7
Stamina - 7
Basic Attacks - 16
Strike Supers - 0
Ki Blast Supers - 22
Total - 57

So the end result would look like:

LVL 1
stat array

LVL 2
stat array

...

LVL 80
stat array

Since the character receives a variable number of stats per level we have to have a hardcoded array and change our distribution based on that, along with what has already been used and what should be used.

<?php
class Stats {

    public function show_level_proportions( $build_stats ) {

        $build_stat_labels = [ 'max_health', 'max_ki', 'max_stamina', 'basic_attack', 'strike_supers', 'ki_blast_supers' ];
        $build = array_combine($build_stat_labels, $build_stats);

        $stat_percents = $this->calculate_stat_percents($build);
        $get_stats_per_lvl = $this->get_stats_per_lvl($stat_percents, $build);

        return $get_stats_per_lvl;
    }

    //Stats given from levels 1-20
    private $incremental_points = [
            0, //1
            2, //2
            3, //3
            3, //4
            3, //5
            3, //6
            3, //7
            3, //8
            3, //9
            3, //10
            3, //11
            3, //12
            3, //13
            3, //14
            3, //15
            3, //16
            3, //17
            3, //18
            3, //19
            4, //20 total: 57
        ];

    private function calculate_stat_percents( $build_stats ) {
        $stat_sum = array_sum( $build_stats );

        foreach ( $build_stats as $key => $value ) {
            $calculated_stat = $this->calc_percents($stat_sum, $value);
            $stat_percentages[$key] = $calculated_stat;
        }

        return $stat_percentages;
    }

    private function calc_percents($sum, $stat){
        $product = ( $stat / $sum );
        return round( $product, 8, PHP_ROUND_HALF_UP );
    }

/*================
* This is the problem area
* I can get the percentages for the inputted stats,
* but I don't even know how to start getting and
* distributing the percentages for each level. So
* right now, it's static, only works with the input,
* and doesn't incorporate the $incremental_stats.
================*/
    private function get_stats_per_lvl($percentages, $stats){
        $stats_total = array_sum($this->incremental_points);

        foreach( $percentages as $key => $value ){
            $lvl_twenty_stats[$key] = $stats_total * $value;
            $rounded_lvl_twenty_stats[$key] = round( $lvl_twenty_stats[$key], 0, PHP_ROUND_HALF_UP );
        }

        return $rounded_lvl_twenty_stats;
    }
}

$stat_tracker = new Stats();
print_r( $stat_tracker->show_level_proportions([5, 0, 5, 20, 7, 20]) );

Upvotes: 2

Views: 1321

Answers (1)

Ava
Ava

Reputation: 2429

Okay, so, to answer this, I'll first go over the theory, then the actual implementation in PHP.


Theory

So, for any given moment, you have your current attribute values, and your ideal attribute values.

For both collections of attributes, you can compute the percentage of the total each attribute represents.

So, for example, given an attribute collection like:

{
    "foo": 2,
    "baz": 1,
    "bar": 3,
    "foobar": 0
}

The total points here is 6, so the percentage of the total for each as computed would be (in pseudo-JSON):

{
    "foo": 33.3%,
    "baz": 16.6%,
    "bar": 50.0%,
    "foobar": 0.0%
}

If you compute this percentage list for each attribute collection (the desired and the current), you can then subtract each value of the desired from the current to get the percentage offset that each current value is off by.

So, a negative offset percentage indicates that the given stat that is associated with that offset is below where it should be, a 0 offset means it is exactly distributed evenly for that level, and a positive offset percentage indicates that the offset is above where it should be.

Since we can't distribute parts of a point, there will always be added points that knock it too high, so we can safely ignore the attributes that have a positive percentage offset.

Similarly, attributes with exactly 0 as their percentage offset can also be ignored, as at least at the current moment, that attribute is correctly distributed.

So, we're only really concerned with attributes that have a negative offset percentage, as those are the ones that need to be incremented.

To wrap it all together, for each point assigned, we need to compute the percentage offsets of the current attributes, assign a point to the attribute with the lowest offset percentage, and repeat the process until we are out of points, recording where, and how many, points we assigned in the process.


Example

Let's use the same example as in OP's question. The ideal distribution attributes collection is (attribute names truncated):

{
    basic: 93,
    ss: 0, 
    kbs: 125,
    health: 30,
    ki: 42,
    stamina: 42
}

And the current is a collection of zeroes (because no points have been assigned yet):

{
    basic: 0,
    ss: 0, 
    kbs: 0,
    health: 0,
    ki: 0,
    stamina: 0
}

And we have 2 points to assign.

Point 1

For the first point, we compute the percentage offsets, which looks like:

{
  basic: -0.28012048192771083,
  ss: 0,
  kbs: -0.37650602409638556,
  health: -0.09036144578313253,
  ki: -0.12650602409638553,
  stamina: -0.12650602409638553
}

If we ignore all zeroes/positive values (as described before), and sort the negatives by the lowest negative, we get this result:

[
  { name: 'kbs', value: -0.37650602409638556 },
  { name: 'basic', value: -0.28012048192771083 },
  { name: 'ki', value: -0.12650602409638553 },
  { name: 'stamina', value: -0.12650602409638553 },
  { name: 'health', value: -0.09036144578313253 }
]

With kbs (Ki Blast Supers) as the lowest offset, so, we assign one point to that, and then move on to the next point.

Point 2

Again, let's compute the percentage offsets, with the increased KBS value. Note that KBS is now positive due to the increase.

{
  basic: -0.28012048192771083,
  ss: 0,
  kbs: 0.6234939759036144,
  health: -0.09036144578313253,
  ki: -0.12650602409638553,
  stamina: -0.12650602409638553
}

And then, again, sort by lowest negative, throwing out zeroes and positive values. Note that KBS is now not eligible because it is slightly too high compared to the other values.

[
  { name: 'basic', value: -0.28012048192771083 },
  { name: 'ki', value: -0.12650602409638553 },
  { name: 'stamina', value: -0.12650602409638553 },
  { name: 'health', value: -0.09036144578313253 }
]

So now basic (Basic Attacks) has the lowest negative, and so we assign the final point to it.

So, our total points assigned looks like:

{
  kbs: 1,
  basic: 1
}

Which is correctly assigned based on OP's expected results.

Implementation

As for implementation in PHP, I won't give a 100% working example, but the basic idea is you want a function called something like getPointsDistributionForTarget which takes the current attribute scores, the target attribute score distributions, and the amount of points to assign.

function getPointsDistributionForTarget(current, target, pointsToSpend) { ... }

Within that function, you'll want to loop the amount of times there is points to spend, doing the following each loop:

  1. Computing the sorted array of offset percentages
  2. Take the top (0th index) value and:
  3. Increasing the associated current attribute value and increasing it by one, and
  4. Note down that you increased that attribute by one, in a running list of totals per attribute

Hopefully this helps! Let me know if you have any issues. I have a working implementation in Node (Javascript) I can provide that I used to test this theory and produce the numbers in the example, or I can try to convert that example to PHP if it helps.

Upvotes: 2

Related Questions