Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sorting arrays with uncertain data

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:

enter image description here

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?

like image 897
Miranda Roberts Avatar asked Dec 30 '15 19:12

Miranda Roberts


2 Answers

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;

        }
    }
like image 181
cmorrissey Avatar answered Oct 16 '22 08:10

cmorrissey


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

like image 45
Marcin Nabiałek Avatar answered Oct 16 '22 06:10

Marcin Nabiałek