How to revert a function in PHP? - php

I am building a little game and got stuck in developing the leveling system. I created a function that will exponentially increase the experience required for the next level. However, I am not sure how to turn it around so that I can put in the amount of experience a user has gained and get the corresponding level.
PHP function
function experience($level, $curve = 300) {
// Preset value to prevent notices
$a = 0;
// Calculate level cap
for ($x = 1; $x < $level; $x++) {
$a += floor($x+$curve*pow(2, ($x/7)));
}
// Return amount of experience
return floor($a/4);
}
The issue
I am wondering how I can reverse engineer this function in order to return the correct level for a certain amount of experience.
Using the above function, my code would output the following:
Level 1: 0
Level 2: 83
Level 3: 174
Level 4: 276
Level 5: 388
Level 6: 512
Level 7: 650
Level 8: 801
Level 9: 969
Level 10: 1154
What I am looking for is a way to invert this function so that I can input a certain amount and it will return the corresponding level.
A 1000 experience should return level 9 for example.

Plugging the values into excel and creating a trend line, I got the following equation:
y = 1.17E-09x^3 - 4.93E-06x^2 + 1.19E-02x + 6.43E-02
So your reverse engineered equation would be
function level($xp) {
$a = 1.17e-9;
$b = -4.93e-6;
$c = 0.0119;
$d = 0.0643
return round($a*pow($xp, 3) + $b*pow($xp,2) + $c * $xp + $d);
}
Results are accurate to within 1dp, but if your $curve changes, you'd need to recalculate. I also haven't extended higher than level 10.
Other options include caching the results of the lookup:
$levelXpAmounts = array()
function populateLevelArray($curve=300) {
$levelXpAmounts[$curve] = array();
for($level = $minlevel; $level <= $maxLevel; $level++) {
$levelXpAmounts[$curve][$level] = experience($level);
}
}
//at game load:
populateLevelArray()
Then, your reverse lookup would be
function level($xp, $curve=300) {
if (!array_key_exists($levelXpAmounts, curve)
populateLevelArray($curve);
for($level = $minlevel; $ level <= $maxLevel; $level++) {
if ($xp < $levelXpAmounts[$curve][$level]) {
return $level - 1;
}
}
}
That way, the iteration through all the levels is only done once for each different value of $curve. You can also replace your old experience() function with a (quite likely faster) lookup.
Note: it's been a while since I've written any php, so my syntax may be a little rusty. I apologize in advance for any errors in that regard.

You can do another function called level which uses the experience function to find the level:
function level($experience)
{
for ($level = 1; $level <= 10; $level++) {
if ($experience <= experience($level)) {
return $level;
}
}
}
function experience($level, $curve = 300)
{
$a = 0;
for ($x = 1; $x < $level; $x++) {
$a += floor($x+$curve*pow(2, ($x/7)));
}
return floor($a/4);
}
var_dump(level(1000));

You can clearly work the math here and find a reverse formula. Not sure whether it will be a nice and easy formula, so I would suggest you an alternative approach which is easy to implement.
Precalculate the results for all the levels you realistically want your person to achieve (I highly doubt that you need more than 200 levels, because based on my estimation you will need tens of billions exp points).
Store all these levels in the array: $arr = [0, 83, 174, 276, 388, 512, 650, ...];. Now your array is sorted and you need to find a position where your level should fit.
If you are looking for 400 exp points, you see that it should be inserted after 5-th position - so it is 5-th level. Even a simple loop will suffice, but you can also write a binary search.

This task could be solved in other way. This is method of partial sums.
Let's assume, you have a class , which stores an array of exponential values calculated by function:
function formula($level, $curve){ return floor($level+$curve*pow(2, ($level/7)));}
$MAX_LEVEL = 90;
function calculateCurve($curve){
$array = [];
for($i =0; $i< $MAX_LEVEL; $i++) $array.push(formula($i, $curve));
return $array;
}
Now we can calculate experience, needed for a level:
$curve = calculateCurve(300);
function getExperienceForLevel($level, $curve){
$S = 0;
for($i =0; $i < level; $i++) $S += $curve[$i];
}
And calculate level for experience:
function getLevelForExperience($exp, $curve){
for($i =0; $i < $MAX_LEVEL; $i++){
$exp -= $curve[$i];
if($exp < 0) return $i-1;
}
return $MAX_LEVEL;
}
I assume there could index problems - I didn't tested the code, but I suppose that main idea is clearly explained.
Pros:
Code cleaner, There no magic numbers and interpolation coeficients.
You can easy change your learning curve.
Possibility to improve and make calculating functions as O(1);
Cons:
There is an $curve array to store, or calculate somewhere.
Also. you could make even more advanced version of this:
function calculateCurve($curve){
$array = [];
$exp = 0;
for($i =0; $i< $MAX_LEVEL; $i++) {
$exp += formula($i, $curve);
$array.push($exp);
}
return $array;
}
Now calculating experience have O(1) complexity;
function getExperienceForLevel($level, $curve){
return $curve[min($MAX_LEVEL, $level)];
}

Perhaps not the best way, but it's working.
function level($experience, $curve = 300)
{
$minLevel = 1;
$maxLevel = 10;
for($level = $minLevel; $level <= $maxLevel; $level++)
{
if(experience($level, $curve) <= $experience && $experience < experience($level + 1, $curve))
{
return $level;
}
}
return $maxLevel;
}

Related

Reverse custom function PHP from experience to level

I have a function where i input a level, and it returns the XP:
this is gotten from runescape 1-99 formula:
function experience($L) {
$a=0;
for($x=1; $x<$L; $x++) {
$a += floor($x+300*pow(2, ($x/7)));
}
return floor($a/4);
}
this means:
level 54 would return 150872 XP.
But, how would i go the other way around, to input 150872 and make it return 54?
and, whats the way to go when xp might be 150873, but its still level 54 to return?
How would i approatch?
Wants:
experience(152439) -> 54
One inefficient but easy solution is to just continuously call the experience function in a loop, increasing the level each time, until you reach a level that returns an experience value above the one you are looking for, and then return the level before that:
function level($experience) {
$returned = 0;
$level = 0;
while ($returned <= $experience) {
$level++;
$returned = experience($level);
}
return $level - 1;
}
Demo: http://sandbox.onlinephpfunctions.com/code/820d659feb28a00dd87a21d01bd2414cbc66d300

combinations without duplicates using php

I need all possible combinations in math sense (without duplicates) where n=30 and k=18
function subcombi($arr, $arr_size, $count)
{
$combi_arr = array();
if ($count > 1) {
for ($i = $count - 1; $i < $arr_size; $i=$i+1) {
$highest_index_elem_arr = array($i => $arr[$i]);
foreach (subcombi($arr, $i, $count - 1) as $subcombi_arr)
{
$combi_arr[] = $subcombi_arr + $highest_index_elem_arr;
}
}
} else {
for ($i = $count - 1; $i < $arr_size; $i=$i+1) {
$combi_arr[] = array($i => $arr[$i]);
}
}
return $combi_arr;
}
function combinations($arr, $count)
{
if ( !(0 <= $count && $count <= count($arr))) {
return false;
}
return $count ? subcombi($arr, count($arr), $count) : array();
}
$numeri="01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30";
$numeri_ar=explode(".",$numeri);
$numeri_ar=array_unique($numeri_ar);
for ($combx = 2; $combx < 19; $combx++)
{
$combi_arr = combinations($numeri_ar, $combx);
}
print_r($combi_arr);
It works but it terminates with an out of memory error, of course, number of combinations is too large.
Now I do not need exactly all the combinations. I need only a few of them.
I'll explain.
I need this work for a statistical study over Italian lotto.
I have the lotto archive in this format saved in $archivio array
...
35.88.86.03.54
70.72.45.18.09
55.49.35.30.43
15.52.49.41.72
74.26.54.77.90
33.14.56.42.11
08.79.41.01.52
82.33.32.83.43
...
A full archive is available here
https://pastebin.com/tut6kFXf
newer extractions are on top.
I tried (unsuccessfully) to modify the function to do this
for each 18 numbers combination found by the function combinations, the function should check if there are min. 3 numbers in one of the first 30 rows of $archivio. If "yes", the combination must not be saved in combination array, this combination has no statistical value for my need. If "no", the combination must be saved in combination array, this combination has great statistical value for my need.
In this way the total combinations will be no more than some hundred or thousand and I will avoid the out of memory and I'll have what I need.
The script time will be surely long but there should not be out of memory using the way above.
Anyone is able to help me in this ?
Thank you

How to generate random numbers to produce a non-standard distributionin PHP

I've searched through a number of similar questions, but unfortunately I haven't been able to find an answer to this problem. I hope someone can point me in the right direction.
I need to come up with a PHP function which will produce a random number within a set range and mean. The range, in my case, will always be 1 to 100. The mean could be anything within the range.
For example...
r = f(x)
where...
r = the resulting random number
x = the mean
...running this function in a loop should produce random values where the average of the resulting values should be very close to x. (The more times we loop the closer we get to x)
Running the function in a loop, assuming x = 10, should produce a curve similar to this:
+
+ +
+ +
+ +
+ +
Where the curve starts at 1, peeks at 10, and ends at 100.
Unfortunately, I'm not well versed in statistics. Perhaps someone can help me word this problem correctly to find a solution?
interesting question. I'll sum it up:
We need a funcion f(x)
f returns an integer
if we run f a million times the average of the integer is x(or very close at least)
I am sure there are several approaches, but this uses the binomial distribution: http://en.wikipedia.org/wiki/Binomial_distribution
Here is the code:
function f($x){
$min = 0;
$max = 100;
$curve = 1.1;
$mean = $x;
$precision = 5; //higher is more precise but slower
$dist = array();
$lastval = $precision;
$belowsize = $mean-$min;
$abovesize = $max-$mean;
$belowfactor = pow(pow($curve,50),1/$belowsize);
$left = 0;
for($i = $min; $i< $mean; $i++){
$dist[$i] = round($lastval*$belowfactor);
$lastval = $lastval*$belowfactor;
$left += $dist[$i];
}
$dist[$mean] = round($lastval*$belowfactor);
$abovefactor = pow($left,1/$abovesize);
for($i = $mean+1; $i <= $max; $i++){
$dist[$i] = round($left-$left/$abovefactor);
$left = $left/$abovefactor;
}
$map = array();
foreach ($dist as $int => $quantity) {
for ($x = 0; $x < $quantity; $x++) {
$map[] = $int;
}
}
shuffle($map);
return current($map);
}
You can test it out like this(worked for me):
$results = array();
for($i = 0;$i<100;$i++){
$results[] = f(20);
}
$average = array_sum($results) / count($results);
echo $average;
It gives a distribution curve that looks like this:
I'm not sure if I got what you mean, even if I didn't this is still a pretty neat snippet:
<?php
function array_avg($array) { // Returns the average (mean) of the numbers in an array
return array_sum($array)/count($array);
}
function randomFromMean($x, $min = 1, $max = 100, $leniency = 3) {
/*
$x The number that you want to get close to
$min The minimum number in the range
$max Self-explanatory
$leniency How far off of $x can the result be
*/
$res = [mt_rand($min,$max)];
while (true) {
$res_avg = array_avg($res);
if ($res_avg >= ($x - $leniency) && $res_avg <= ($x + $leniency)) {
return $res;
break;
}
else if ($res_avg > $x && $res_avg < $max) {
array_push($res,mt_rand($min, $x));
}
else if ($res_avg > $min && $res_avg < $x) {
array_push($res, mt_rand($x,$max));
}
}
}
$res = randomFromMean(22); // This function returns an array of random numbers that have a mean close to the first param.
?>
If you then var_dump($res), You get something like this:
array (size=4)
0 => int 18
1 => int 54
2 => int 22
3 => int 4
EDIT: Using a low value for $leniency (like 1 or 2) will result in huge arrays, since testing, I recommend a leniency of around 3.

PHP - How to count a generators yields

Using PHP >= 5.5 if we have a method that yielded values, what would be the best method in counting these values?
What I was expecting was to be able to convert a Generator to an array and count that, however it would return an empty array. Count() also does not work as the Generator is reported as empty despite not being empty.
I'm baffled with this. If you don't need to count a generators yields then it's a nice feature otherwise I don't see much of a point for it. There is a way to detect if a generator is empty, this is by using the key() method and if it returns NULL then there are either no yields or the generator has already been iterated through which would mean the current pointer is null.
If you have to do it, following as a on-liner of native functions:
count(iterator_to_array($generator, false));
However, take care: After this your $generator is executed and consumed. So if you would put that same $generator into a foreach in a following line, it would loop 0 times.
Generators are by design highly dynamic (in contrast to fixed data structures like arrays), thats why they don't offer ->count() or ->rewind().
You should understand, that generator isn't data structure - it's an instance of Generator class and, actually, it's special sort of Iterator. Thus, you can't count its items directly (to be precise - that's because Generator class implements only Iterator interface, and not Countable interface. To be honest, I can't imagine how can it implement that)
To count values with native generator you'll have to iterate through it. But that can not be done in common sense - because in most cases it's you who'll decide how many items will be yielded. Famous xrange() sample from manual:
function xrange($start, $limit, $step = 1) {
if ($start < $limit) {
if ($step <= 0) {
throw new LogicException('Step must be +ve');
}
for ($i = $start; $i <= $limit; $i += $step) {
yield $i;
}
} else {
if ($step >= 0) {
throw new LogicException('Step must be -ve');
}
for ($i = $start; $i >= $limit; $i += $step) {
yield $i;
}
}
}
-as you can see, it's you who must define borders. And final count will depend from that. Iterating through generator will have sense only with static-borders defined generator (i.e. when count of items is always static - for example, defined inside generator strictly). In any other case you'll get parameter-dependent result. For xrange():
function getCount(Generator $functor)
{
$count = 0;
foreach($functor as $value)
{
$count++;
}
return $count;
}
-and usage:
var_dump(getCount(xrange(1, 100, 10)));//10
var_dump(getCount(xrange(1, 100, 1)));//100
-as you can see, "count" will change. Even worse, generator hasn't to be finite. It may yield infinite set of values (and borders are defined in external loop, for example) - and this is one more reason which makes "counting" near senseless.
Actually, it depends in which case you are :
Case 1 : I can't count before iterating and I care about values
// The plain old solution
$count = 0;
foreach($traversable as $value) {
// Do something with $value, then…
++$count;
}
Case 2 : I can't count before iterating but I don't care about values
// let's iterator_count() do it for me
$count = iterator_count($traversable);
Case 3 : I can count before iterating but I don't care about values
I try not to use generators.
For example (with SQL backends) :
SELECT count(1) FROM mytable; // then return result
is better than
SELECT * FROM mytable; // then counting results
Other example (with xrange from Alma Do) :
// More efficient than counting by iterating
function count_xrange($start, $limit, $step = 1) {
if (0 === $step) throw new LogicException("Step can't be 0");
return (int)(abs($limit-$start) / $step) + 1;
}
Case 4 : I can count before iterating and I care about values
I can use a generator AND a count function
$args = [0,17,2];
$count = count_xrange(...$args);
$traversable = xrange(...$args);
Case 5 : Case 4, and I want all in one object
I can "decorate" an Iterator to make a Countable Iterator
function buildCountableIterator(...$args) {
$count = count_xrange(...$args);
$traversable = xrange(...$args);
return new class($count, $traversable) extends \IteratorIterator implements \Countable {
private $count;
public function __construct($count, $traversable) {
parent::__construct($traversable);
$this->count = $count;
}
public function count() {
return $this->count;
}
}
}
$countableIterator = buildCountableIterator(1, 24, 3);
// I can do this because $countableIterator is countable
$count = count($countableIterator);
// And I can do that because $countableIterator is also an Iterator
foreach($countableIterator as $item) {
// do something
}
Sources :
http://php.net/manual/en/function.iterator-count.php
http://php.net/manual/en/class.countable.php
http://php.net/manual/en/class.iteratoriterator.php
http://php.net/manual/en/language.oop5.anonymous.php
While you can't use count() you can use a reference to set the count to make it accessible to the outside world.
function generate(&$count = 0) {
// we have 4 things
$count = 4;
for($i = 0; $i < $count; $i++) {
yield $i;
}
}
$foo = generate($count);
echo $count; // 4
foreach ($foo as $i) {
echo $i;
}
Downside to this is it won't tell you how many remain but how many it started with.

Pearson correlation in PHP

I'm trying to implement the calculation of correlation coefficient of people between two sets of data in php.
I'm just trying to do the porting python script that can be found at this url
http://answers.oreilly.com/topic/1066-how-to-find-similar-users-with-python/
my implementation is the following:
class LB_Similarity_PearsonCorrelation implements LB_Similarity_Interface{
public function similarity($user1, $user2){
$sharedItem = array();
$pref1 = array();
$pref2 = array();
$result1 = $user1->fetchAllPreferences();
$result2 = $user2->fetchAllPreferences();
foreach($result1 as $pref){
$pref1[$pref->item_id] = $pref->rate;
}
foreach($result2 as $pref){
$pref2[$pref->item_id] = $pref->rate;
}
foreach ($pref1 as $item => $preferenza){
if(key_exists($item,$pref2)){
$sharedItem[$item] = 1;
}
}
$n = count($sharedItem);
if ($n == 0) return 0;
$sum1 = 0;$sum2 = 0;$sumSq1 = 0;$sumSq2 = 0;$pSum = 0;
foreach ($sharedItem as $item_id => $pre) {
$sum1 += $pref1[$item_id];
$sum2 += $pref2[$item_id];
$sumSq1 += pow($pref1[$item_id],2);
$sumSq2 += pow($pref2[$item_id],2);
$pSum += $pref1[$item_id] * $pref2[$item_id];
}
$num = $pSum - (($sum1 * $sum2) / $n);
$den = sqrt(($sumSq1 - pow($sum1,2)/$n) * ($sumSq2 - pow($sum2,2)/$n));
if ($den == 0) return 0;
return $num/$den;
}
}
clarification to better understand the code, the method fetchAllPreferences return back a set of objects that are actually the items, turns them into an array for ease of management
I'm not sure that this implementation is correct, in particular I have some doubts about the correctness of the calculation of the denominator.
any advice is welcome.
thanks in advance!
This is my solution:
function php_correlation($x,$y){
if(count($x)!==count($y)){return -1;}
$x=array_values($x);
$y=array_values($y);
$xs=array_sum($x)/count($x);
$ys=array_sum($y)/count($y);
$a=0;$bx=0;$by=0;
for($i=0;$i<count($x);$i++){
$xr=$x[$i]-$xs;
$yr=$y[$i]-$ys;
$a+=$xr*$yr;
$bx+=pow($xr,2);
$by+=pow($yr,2);
}
$b = sqrt($bx*$by);
if($b==0) return 0;
return $a/$b;
}
http://profprog.ru/korrelyaciya-na-php-php-simple-pearson-correlation/
Your algorithm looks mathematically correct but numerically unstable. Finding the sum of squares explicitly is a recipe for disaster. What if you have numbers like array(10000000001, 10000000002, 10000000003)? A numerically stable one-pass algorithm for calculating the variance can be found on Wikipedia, and the same principle can be applied to computing the covariance.
Easier yet, if you don't care much about speed, you could just use two passes. Find the means in the first pass, then compute the variances and covariances using the textbook formula in the second pass.
try my package here
http://www.phpclasses.org/browse/package/5854.html

Categories