Sorting arrays with uncertain data - php
I am crafting a script for a browser game that will generate a random animal for the player to battle with anywhere from 0-5 markings. The markings on that animal are randomly generated and are fed into a custom imagick function, which will add them in order as they appear in the array.
While the markings are randomly decided, there are a lot of rules to how they are supposed to appear on an animal, for instance markings in the "full body" region show above markings in the "belly" region. To better explain, I'll attach an image of the tester so far:
So to break down the 5 markings on this randomly generated animal, the eyeshadow marking belongs to eye region, undertail belongs to tail, streaks belongs to fullbody, appaloosa belongs to back, and okapi belongs to legs. The order right now is just added as the script looped through the database and randomly selected markings, so okapi (the stripes on the legs) is on top since it was the last on in the array, and the last one added. But following the order rules, the last one in the array should have been streaks (the horizontal streaks across the body), since fullbody markings go on top.
Here is the code the selects markings, this is done using the Laravel engine:
// Determine number of markings
$num = mt_rand(1,10);
if ($num == 1) {
$markingNum = 0;
} elseif ($num > 1 && $num < 4) {
$markingNum = 1;
} elseif ($num > 4 && $num < 6) {
$markingNum = 2;
} elseif ($num > 6 && $num < 8) {
$markingNum = 3;
} elseif ($num > 8 && $num < 10) {
$markingNum = 4;
} else {
$markingNum = 5;
}
// Calculate Marking type and color
$markings = array();
if ($markingNum > 0) {
for ($m = 0 ; $m < $markingNum; $m++) {
// Set color values (pulls from the "pallet" selected earlier in the code, which will determine the range of color that marking can be)
if ($m == 1) {
$pal = $pallet->marking1;
} elseif ($m == 2) {
$pal = $pallet->marking2;
} elseif ($m == 3) {
$pal = $pallet->marking3;
} elseif ($m == 4) {
$pal = $pallet->marking4;
} else {
$pal = $pallet->marking5;
}
// Pull previous marking info
if (count($markings) != 0) {
$previous = DataMarking::whereIn('name', array_keys($markings))->get();
// This pulls the regions of the current markings in the array so it won't select a region that already has a marking.
foreach ($previous as $p) {
$regions[$p->region] = $p->name;
}
// Uncommon marking (10% chance)
$r = mt_rand(1, 10);
if ($r == 10) {
$marking = DataMarking::where('rarity', 1)
->where('public', 1)
->whereNotIn('name', array_keys($markings))
->whereNotIn('region', array_keys($regions))
->orderByRaw("RAND()")
->first();
// Common markings
} else {
$marking = DataMarking::where('rarity', 0)
->where('public', 1)
->whereNotIn('name', array_keys($markings))
->whereNotIn('region', array_keys($regions))
->orderByRaw("RAND()")
->first();
}
// Colors marking
if ($pal == 0) {
$markingColor = rand_color();
} else {
$range = ColorRange::where('id', $pal)->firstOrFail();
$markingColor = "#" . mixRange(substr($range->start_hex, 1), substr($range->end_hex, 1));
}
$markings[$marking->name] = $markingColor;
} else {
// Uncommon marking (10% chance)
$r = mt_rand(1, 10);
if ($r == 10) {
$marking = DataMarking::where('rarity', 1)
->where('public', 1)
->orderByRaw("RAND()")->first();
// Common marking
} else {
$marking = DataMarking::where('rarity', 0)
->where('public', 1)
->orderByRaw("RAND()")->first();
}
// Colors marking
if ($pal == 0) {
$markingColor = rand_color();
} else {
$range = ColorRange::where('id', $pal)->firstOrFail();
$markingColor = "#" . mixRange(substr($range->start_hex, 1), substr($range->end_hex, 1));
}
$markings[$marking->name] = $markingColor;
}
}
}
I figure I can accomplish this with a lot of complex if statements but it just doesn't seem like an elegant solution to me. In addition, there is an exception: 'Gradient', a fullbody marking, goes underneath everything even though it is a full body marking. So far it is the only marking with this sort of exception to it, though.
I have tried using various sort functions offered by PHP but I am not having much luck. uksort seems the most promising, but since the value we are sorting by exists in the database and not in the array we are sorting (the imagick function has to be fed a marking => color array format), it's proving difficult to work with.
Tl;dr: I need to reorder an array that an uncertain amount of data based off of values that exist in the DB for the keys (the region for the markings). What's the most elegant way to accomplish this?
Here are some optimizations to your code, there are comments inline to describe what was done. This is obviously not finished as there are some things Marcin pointed out in his answer that would be better.
// Determine number of markings
$num = mt_rand(1,10);
// Removed redundent $num > X as the conditions were already meet that it was > X by the previous if statement
if ($num == 1) {
$markingNum = 0;
} else if ($num < 4) {
$markingNum = 1;
} else if ($num < 6) {
$markingNum = 2;
} else if ($num < 8) {
$markingNum = 3;
} else if ($num < 10) {
$markingNum = 4;
} else {
$markingNum = 5;
}
// Calculate Marking type and color
$markings = array();
if ($markingNum > 0) {
for ($m = 1 ; $m <= $markingNum; $m++) { // incrimented to 1 and <= so we can dynamically select elements
// Set color values (pulls from the "pallet" selected earlier in the code, which will determine the range of color that marking can be)
$pal = $pallet->{'marking' . $m}; // Removed if/else and replaced with a dynamic variable
// Uncommon marking (10% chance)
$r = mt_rand(1, 10);
// removed duplicate database selections for a simple $rarity variable that accomplishes the same task
if ($r == 10) {
$rarity = 1;
} else {
$rarity = 0;
}
$marking = DataMarking::where('rarity', $rarity)
->where('public', 1)
->whereNotIn('name', array_keys($markings))
->whereNotIn('region', $regions)
->orderByRaw("RAND()")
->first();
// Colors marking
if ($pal == 0) {
$markingColor = rand_color();
} else {
$range = ColorRange::where('id', $pal)->firstOrFail();
$markingColor = "#" . mixRange(substr($range->start_hex, 1), substr($range->end_hex, 1));
}
$markings[$marking->name] = $marking; // adds all of the marking data, this is where you could have a z-index in the database
$markings[$marking->name] = $markingColor; // add your color to your marking data
$regions[] = $marking->region;
}
}
I won't answer your question, but looking at your code there are a lot of space for improvements.
Consider this:
if ($m == 1) {
$pal = $pallet->marking1;
} elseif ($m == 2) {
$pal = $pallet->marking2;
} elseif ($m == 3) {
$pal = $pallet->marking3;
} elseif ($m == 4) {
$pal = $pallet->marking4;
} else {
$pal = $pallet->marking5;
}
It could be changed for something much simpler:
$pa1 = (in_array($m,range(1,4))) ? $pallet->marking{$m} : $pallet->marking5;
Same for:
if ($r == 10) {
$marking = DataMarking::where('rarity', 1)
->where('public', 1)
->whereNotIn('name', array_keys($markings))
->whereNotIn('region', array_keys($regions))
->orderByRaw("RAND()")
->first();
// Common markings
} else {
$marking = DataMarking::where('rarity', 0)
->where('public', 1)
->whereNotIn('name', array_keys($markings))
->whereNotIn('region', array_keys($regions))
->orderByRaw("RAND()")
->first();
}
if could be rewritten to:
$marking = DataMarking::where('rarity', ($r == 10) ? 1 : 0)
->where('public', 1)
->whereNotIn('name', array_keys($markings))
->whereNotIn('region', array_keys($regions))
->orderByRaw("RAND()")
->first();
Of course in above ORDER BY RAND() might be not the best solution because of performance reasons.
You should really care about amount of your code and duplication or you'll soon be lost in what you are doing
Related
Comparing results in Gravity Forms
I've created a questionnaire with three categories. Each possible answer in the questionnaire corresponds with one of the categories. I have three admin numeric fields where I add the number of answers selected from each category (fields 121, 122, 123). This part of the form is working. I'd like to compare the totals from these fields to see which is greatest and then return that result to a hidden field (field 126). So far my code is triggering a critical error when I submit the form. add_filter("gform_pre_submission_9", "find_highest_percent"); function find_highest_percent ($vata, $pitta, $kapha, $form) { $total = 0; $vata = $_POST["input_121"] ; $pitta = $_POST["input_122"] ; $kapha = $_POST["input_123"] ; $total = $vata + $pitta + $kapha; $vata_percent = ($vata / $total) * 100; $pitta_percent = ($pitta / $total) * 100; $kapha_percent = ($kapha / $total) * 100; if (abs($vata - $kapha) <= 10) { $result = "Vata-Kapha"; } elseif (abs($vata - $pitta) <= 10) { $result = "Vata-Pitta"; } elseif (abs($pitta - $kapha) <= 10) { $result = "Pitta-Kapha"; } elseif (abs($vata - $pitta) <= 10 && abs($vata - $kapha) <= 10 && abs($pitta - $kapha) <= 10) { $result = "Tri-Doshic"; } else { if ($vata > $pitta && $vata > $kapha) { $result = "Vata"; } elseif ($pitta > $vata && $pitta > $kapha) { $result = "Pitta"; } else { $result = "Kapha"; } } $_POST["input_126"] = $result; } I've tested by removing all calculations and simply returning the number 100 but this also triggers the error. Grateful for any suggestions.
Does it help to set the variables to integers? $vata = (int)$_POST["input_121"] ; $pitta = (int)$_POST["input_122"] ; $kapha = (int)$_POST["input_123"] ;
Can I have more than one while(!feof) loop in a program?
If I'm trying to display a table (which I've accomplished with a while loop) but also display a count underneath it. Do I add another while loop? Or a seperate for loop? How would I do that? I need to count the number of performances (Ive got that working) but it wont tally the number of performances in Asheville. How do I target that variable by itself? > <?php print ("<h1>Upcoming Performances in 2015</h1>"); print > ("<table border =\"1\">"); print("<tr><th align = > \"left\">Date</th><th align = \"left\">Venue</th><th align = > \"left\">City</th><th align = \"right\">Ticket Price</th></tr>"); > > $count = 0; $ashevilleCount = 0; $eventFile = > fopen("performances.txt", "r"); $schedule = fgets($eventFile); > > > while(!feof($eventFile)) { list($date, $venue, $city, $ticketPrice) = explode(":", $schedule); > print("<tr><td>$date</td>"); print("<td>$venue</td>"); print("<td>$city</td>"); print("<td>$ticketPrice</td>"); > $schedule = fgets($eventFile); > } > > for($count = 1; $count <= 5; $count = $count + 1) { $total = fgets($eventFile); $count = $count + $total; > } > if ($city == Asheville) > $ashevilleCount = $ashevilleCount + $count; > > > > > > fclose($eventFile); > print ("</table>"); > > print ( "<p class=\"alert\">Lower cost venues are marked with > *</p>"); print ("<p>NUMBER OF PERFORMANCES: $count</p>"); print ("<p>NUMBER OF PERFORMANCES IN ASHEVILLE: $ashevilleCount</p>"); > > > > > ?>
You need to take a look at your if statements. if($condition === true){ //executes if $condition is true } elseif($condition === 1) { //executes if $condition is 1 } elseif($condition === 2 || $condition === 3){ //executes if $condition is 2 OR condition is 3 } elseif($condition === 4 && $otherCondition !== "foo"){ //executes if $condition is 4 AND $otherCondition is NOT "foo" } else { //executes if no other statements are true } This piece of code: elseif ($charType == human or dwarf and $goldSpent <= 10) $supplyTokens = $_POST['supplyTokens'] + 10; Needs to look like: } elseif( ( $charType=="human" || $charType=="dwarf" ) && $goldSpent <= 10) { $supplyTokens = $_POST['supplyTokens'] + 10; } Remember: || = "or" && = "and" test != "test" - make sure your strings are enclosed in quotation marks See: http://php.net/manual/en/control-structures.if.php http://php.net/manual/en/control-structures.elseif.php http://php.net/manual/en/language.operators.comparison.php
Here's your code cleaned up. What was done: Changed all the print() commands to concatenated echos. Fixed the conditionals You don't need to check for $goldSpent <= 10 in your elseifs as you have already checked that by not being in $goldSpent > 10 I personally prefer || and && as opposed to or and and Added curly brackets {} Thing to consider: What would happen if any of those $_POST values are empty?? <?php $charName = $_POST['charName']; $charType = $_POST['charType']; $healthTokens = $_POST['healthTokens']; $expTokens = $_POST['expTokens']; $supplyTokens = $_POST['supplyTokens']; $goldSpent = $healthTokens / 10 + $expTokens / 2 + $supplyTokens / 25; if ($goldSpent > 10) { echo "<h1>HEY THERE, $charName!</h1>" . "<p>YOU SPENT MORE GOLD THAN YOU HAVE!</p>" . "<p>GO BACK AND TRY THAT AGAIN - YOU HAVE 10 GOLD PIECES..</p>"; } elseif ($charType == 'elf') { $healthTokens = $_POST['healthTokens'] + 5; } elseif ($charType == 'wizard') { $expTokens = $_POST['expTokens'] + 2; } elseif ($charType == 'human' || $charType == 'dwarf') { $supplyTokens = $_POST['supplyTokens'] + 10; } $totalGold = 10; $goldLeft = $totalGold - $goldSpent; echo "<h1>You have created $charName the $charType!</h1>" . "<p>$charName has <strong>$healthTokens</strong> health tokens," . "<strong>$expTokens</strong> experience tokens, and" . "<strong>$supplyTokens</strong> supply tokens.</p>" . "<p>You received some bonus tokens! :)</p>" . "<p>$charName has spent <strong>$goldSpent</strong> gold pieces, " . "and has <strong>$goldLeft</strong> gold pieces left.</p>";
Php level and exp system
How can i make a level system where i have 2 table, one for the level and another for amount of exp earned? I want to be able to manage the different exp requierments myself, like level 2 will need 340exp and level 3 need 450exp. I dont want to set one exp amount and then multiply it. I want to manage the whole system. I also want to set default level and max level, and give exp directly to the database column without too much problem (for forum posts etc). I have seen a few questions here but i find them outdated or just not what im looking for. PS: sorry for bad english and bad explenation.
I found a realy good solution and was able to rewrite it to work with my setup. if anyone is interested i will leave the original link and my edit of it bellow. my edit: <?php // Connect to your database like you normally do, then get any value into the $count variable $count = $exp; if($level == 0){ $lvl = 1; }else{ $lvl = $level; } if ($count >= 12800) { $lvl = 10; } else if ($count >= 6400) { $lvl = 9; } else if ($count >= 3200) { $lvl = 8; } else if ($count >= 1600) { $lvl = 7; } else if ($count >= 800) { $lvl = 6; } else if ($count >= 400) { $lvl = 5; } else if ($count >= 200) { $lvl = 4; } else if ($count >= 100) { $lvl = 3; } else if ($count >= 50) { $lvl = 2; } // Render stars or any graphics/images according to dynamic $lvl number $stars = ""; $i = 1; while ($i <= $lvl) { $stars .= "★"; $i++; } echo "level $lvl"; ?> Original link: https://www.developphp.com/video/PHP/Experience-Level-Evaluation-Programming-Tutorial
2D Cluster algorithm
I am analyzing images and need a fast clustering algorithm which searches for the center of the biggest center. A sample data set might look like this: $x = array(6,9,7,0,0,0,4,0,0,6,6,3); As you see there are 3 clusters in the array. The result I am looking for is the array position of the center of the cluster with the highest sum. In the example above this would be 1 as $x[1] is the center of the biggest cluster 6+9+7(=22). Any ideas?
Whichever way you go, you'll have to walk through the array at least once. The following algorithm does this in one pass without any additional sorting/searching - although I admit that it still may not be the most efficient one. Note that if the biggest cluster has an even number of elements, then it'll return the "lower" mid-point (e.g. for a cluster with indices 0, 1, 2, 3 it will return 1) - this can be easily adjusted in the last line of computation ($mid = ...). $input = array(6,9,7,0,0,0,4,0,0,6,6,3); $clust = array(0, 0, 0); $st = -1; $sum = 0; for($i=0; $i<count($input); $i++) { if($input[$i] == 0) { if($i == 0) { continue; } elseif($input[$i - 1] == 0) { continue; } else { if($clust[2] < $sum) { $clust = array($st, $i - 1, $sum); } } } else { if($i == 0 || $input[$i - 1] == 0) { $st = $i; $sum = 0; } $sum += $input[$i]; } } if(end($input) != 0 && $clust[2] < $sum) { $clust = array($st, $i - 1, $sum); } if($clust[2] > 0) { $mid = (int)(($clust[1] - $clust[0]) / 2); echo $clust[0] ."->". $mid ."->" . $clust[1] ." = ". $clust[2]; } else { echo "No clusters found"; }
Point-in-Polygon PHP Errors
I am using a point-in-polygon check in php, but I am getting major errors - as in points that are not in the polygon are coming up as inside. My basic functions are typed out below (found here, modified from a class to a simple function: http://www.assemblysys.com/dataServices/php_pointinpolygon.php). The only thing I can think of is some kind of rounding errors someplace? As one example, I am trying to determine whether a point is in Central Park, a simple square, but I get positives from points outside the park. Thanks for any insight, -D $central_park = array('40.768109,-73.981885', '40.800636,-73.958067', '40.796900,-73.949184', '40.764307,-73.972959'); $test_points = array('40.7546755,-73.9758343', '40.764405,-73.973951', '40.7594219,-73.9733896', '40.768137896318315,-73.9814176061', '40.7982394,-73.9523718', '40.685135,-73.973562', '40.7777062,-73.9632719', '40.764109,-73.975948', '40.758908,-73.9813128', '40.7982782,-73.9525028', '40.7463886,-73.9817654', '40.7514592,-73.9760405', '40.7514592,-73.9760155', '40.7514592,-73.9759905', '40.7995079,-73.955431', '40.7604354,-73.9758778', '40.7642878,-73.9730075', '40.7655335,-73.9800484', '40.7521678,-73.9777978', '40.7521678,-73.9777728') function pointStringToCoordinates($pointString) { $coordinates = explode(",", $pointString); return array("x" => trim($coordinates[0]), "y" => trim($coordinates[1])); } function isWithinBoundary($point,$polygon){ $point = pointStringToCoordinates($point); $vertices = array(); foreach ($polygon as $vertex) { $vertices[] = pointStringToCoordinates($vertex); } // Check if the point is inside the polygon or on the boundary $intersections = 0; $vertices_count = count($vertices); for ($i=1; $i < $vertices_count; $i++) { $vertex1 = $vertices[$i-1]; $vertex2 = $vertices[$i]; if ($vertex1['y'] == $vertex2['y'] and $vertex1['y'] == $point['y'] and $point['x'] > min($vertex1['x'], $vertex2['x']) and $point['x'] < max($vertex1['x'], $vertex2['x'])) { // Check if point is on an horizontal polygon boundary $result = TRUE; } if ($point['y'] > min($vertex1['y'], $vertex2['y']) and $point['y'] <= max($vertex1['y'], $vertex2['y']) and $point['x'] <= max($vertex1['x'], $vertex2['x']) and $vertex1['y'] != $vertex2['y']) { $xinters = ($point['y'] - $vertex1['y']) * ($vertex2['x'] - $vertex1['x']) / ($vertex2['y'] - $vertex1['y']) + $vertex1['x']; if ($xinters == $point['x']) { // Check if point is on the polygon boundary (other than horizontal) $result = TRUE; } if ($vertex1['x'] == $vertex2['x'] || $point['x'] <= $xinters) { $intersections++; } } } // If the number of edges we passed through is even, then it's in the polygon. if ($intersections % 2 != 0) { $result = TRUE; } else { $result = FALSE; } return $result; }
There were a couple of problems with the original code, closing the polygons fixed one of these but the code also gave incorrect results for points on the boundary lines of the polygon. The if..else statement at the end of the isWithinBoundary function only needs to be executed if a point IS NOT on a boundary. As a point on the boundary won't actually intersect the boundary then the count of intersections would always be odd for a boundary point meaning that this final IF statement would always return FALSE for a boundary point. I have tweaked the code a little, this version is a self contained page that has some simple test data and it outputs the decisions being made. <?php $myPolygon = array('4,3', '4,6', '7,6', '7,3','4,3'); $test_points = array('0,0','1,1','2,2','3,3','3.99999,3.99999','4,4','5,5','6,6','6.99999,5.99999','7,7'); echo "The test polygon has the co-ordinates "; foreach ($myPolygon as $polypoint){ echo $polypoint.", "; } echo "<br/>"; foreach ($test_points as $apoint) { echo "Point ".$apoint." is "; if (!isWithinBoundary($apoint,$myPolygon)) { echo " NOT "; } echo "inside the test polygon<br />"; } function pointStringToCoordinates($pointString) { $coordinates = explode(",", $pointString); return array("x" => trim($coordinates[0]), "y" => trim($coordinates[1])); } function isWithinBoundary($point,$polygon) { $result =FALSE; $point = pointStringToCoordinates($point); $vertices = array(); foreach ($polygon as $vertex) { $vertices[] = pointStringToCoordinates($vertex); } // Check if the point is inside the polygon or on the boundary $intersections = 0; $vertices_count = count($vertices); for ($i=1; $i < $vertices_count; $i++) { $vertex1 = $vertices[$i-1]; $vertex2 = $vertices[$i]; if ($vertex1['y'] == $vertex2['y'] and $vertex1['y'] == $point['y'] and $point['x'] > min($vertex1['x'], $vertex2['x']) and $point['x'] < max($vertex1['x'], $vertex2['x'])) { // This point is on an horizontal polygon boundary $result = TRUE; // set $i = $vertices_count so that loop exits as we have a boundary point $i = $vertices_count; } if ($point['y'] > min($vertex1['y'], $vertex2['y']) and $point['y'] <= max($vertex1['y'], $vertex2['y']) and $point['x'] <= max($vertex1['x'], $vertex2['x']) and $vertex1['y'] != $vertex2['y']) { $xinters = ($point['y'] - $vertex1['y']) * ($vertex2['x'] - $vertex1['x']) / ($vertex2['y'] - $vertex1['y']) + $vertex1['x']; if ($xinters == $point['x']) { // This point is on the polygon boundary (other than horizontal) $result = TRUE; // set $i = $vertices_count so that loop exits as we have a boundary point $i = $vertices_count; } if ($vertex1['x'] == $vertex2['x'] || $point['x'] <= $xinters) { $intersections++; } } } // If the number of edges we passed through is even, then it's in the polygon. // Have to check here also to make sure that we haven't already determined that a point is on a boundary line if ($intersections % 2 != 0 && $result == FALSE) { $result = TRUE; } return $result; } ?> You've probably spotted and fixed these problems yourself by now, but this might help other people who find and use this code.
Well, once again I find myself foolishly answering my own foolish question. I was not closing the polygon by appending the first coordinate to the last spot in the array. This caused a very distinctive look of the mismatched points - they all appeared to be spilling out of the polygon from the unbounded end. So this - $central_park = array('40.768109,-73.981885', '40.800636,-73.958067', '40.796900,-73.949184', '40.764307,-73.972959'); Should be this - $central_park = array('40.768109,-73.981885', '40.800636,-73.958067', '40.796900,-73.949184', '40.764307,-73.972959', '40.764307,-73.972959'); And that's how I was dumb today. Thank you.
The problem with your code is that the variable $result is overwritten by this code if ($intersections % 2 != 0) { $result = TRUE; } else { $result = FALSE; } even if $result == TRUE here: if ($xinters == $point['x']) { $result = TRUE; } In the original code there was an 'return' which was correct instead of your is wrong.