tags:

views:

92

answers:

5

I have an array of images. Each image has a "rarity" key that tells me whether it's "common", "uncommon", or "rare". So for instance, the array might look something like this:

Array
(
    [0] => Array
        (
            [image] => photo1.jpg
            [rarity] => common
        )

    [1] => Array
        (
            [image] => photo2.jpg
            [rarity] => uncommon
        )
    .
    .
    .

    [x] => Array
        (
            [image] => photo(x).jpg
            [rarity] => rare
        )
)

I want to select 'y' number of images from the list, almost like creating a deck of cards, but with images instead. The rarity, of course, defines how likely that card will be selected. How do I go about doing this? I figure I start with array_rand(), but am stuck as to where to go with it.

EDIT FOR CLARIFICATION: Each image in the array can only occur in the final deck once.

+1  A: 

Let's say that the common category is 5 times more common than the rare, and the uncommon occurs 2 times more than the rare.

For each image, it is a change of 5 out of 8 that it is common, 2 out of 8 that it is uncommon, and 1 out of eight that it is rare.

Thus:

if ((rand() % 8) < 5)
{
    $key = array_rand($common);
    return $common[$key];
} else {
    etc.
Sjoerd
If you select first based on the *category* rather than the individual *card* probability, it might not lead to the desired result. For instance, if I have 20 common cards and 1 rare card in a deck, what is expected to be the most common card in 100 selections by your method?
Karmastan
@Karmastan Good point, a better way is to multiply the number of cards in the common stack by 5, and the number of cards in the uncommon stack by 2, etc.
Sjoerd
+7  A: 

I would do this with three different arrays. One for each rarity type.

Lets say the probabilities are: common = 70%, uncommon = 25%, rare = 5%.

Your three arrays are $commonArray, $uncommonArray and $rareArray.

Your target deck is $deck.

Then you generate a random number from 1 to 100 and choose one of the three arrays:

<?php
$rand = rand(1,100);
if ($rand <= 70) {
    $cardArray = &$commonArray;
} else if ($rand <= 95) {
    $cardArray = &$uncommonArray;
} else {
    $cardArray = &$rareArray;
}

Now choose one card from the selected array:

$chosen = array_rand($cardArray);
$deck[] = $cardArray[$chosen];
unset($cardArray[$chosen]); //remove the chosen card from the array to prevent duplicates

Repeat this until your $deck is the size you want it to be.

Techpriester
This only needs to be refined to remove chosen cards from the arrays (poster states that any card may only appear once in the deck) and fall back to another array if the randomly chosen array is empty.
Sven Koschnicke
Yes, if it is desired to remove chosen cards from the source arrays, you could add an "unset($cardArray[$chosenIndex])" there. I'll fix that ...
Techpriester
+1 I actually like this approach better than mine, and think if fleshed out it would be better than mine. But the asker didn't say that the cards come in three arrays or give any information about the source of the data, so I didn't do it like this.
Jesse Dhillon
A: 

I would change the system of storing rarity from text to numbers instead, with low numbers being more rare. This way, you can expand out the system next time with just numbers, instead of inventing more categories like "ultra rare" or "beyond ultra rare". The number can be a weight-age. The higher the numbers, the more common it is. If all the frequency of the categories add up to 100%, you can easily write a generic version of Sjoerd's answer.

Also, rather than storing all the cards in one array, like what Sjoerd posted in his answer above, is to have each category being its own array. You could store all the cards in a DB and have function to group them by their rarity.

Extrakun
+2  A: 

Here's my attempt. In $chances you have to define the chances that a common, uncommon or rare card will be chosen to appear in a deck as an int between 0-100. Also you have to provide the deck size.

Presenting an answer where the cards are unique and cannot appear multiple times in the same deck:

$cards = array(/* your array from above */);
$deck = array();
$deck_size = 52;

$chances = array('common' => 90, 'uncommon' => 30, 'rare' => 5);
$max_attempts = 3;
$tmp_cards = $cards; // copied

while(count($deck) < $deck_size) {
  $roll = rand(0, 100);

  for($a = 0; $a < $max_attempts; $a++) {
    $index = array_rand($tmp_cards);
    $card = $tmp_cards[$index];
    $rarity = $card['rarity'];
    $image = $card['image'];

    if(isset($deck[$image])) {
      continue;
    }

    if($roll <= $chances[$rarity]) {
      // if the roll is lower than the required probability
      // we can put this card in
      $deck[$image] = $card;
      unset($tmp_cards[$index]); // delete the card so it's not picked again
      break;
    }
  }
}

$deck = array_values($deck);

Update

Improved the code and provided definite exits to all branches.

Jesse Dhillon
I do need cards/images to be unique, and I like your second solution. I'm testing it now, and it seems that when my selection is large but my pool is small by comparison (say, I end up choosing 90% of the cards anyway), then the script will sometimes take a long time as it's actually having to discard its choices and repick. Is there a way of creating a kind of failsafe to keep the script from timing out?
saikofish
be careful with this code as it theoretically might never terminate.
Sven Koschnicke
In my experience, the PHP random number generator will spit out the same number many times consecutively so that may be what's happening. We could do a couple things: 1) copy the input array to a new array, and `array_rand` the new one, then delete the selected element, and/or 2) if a certain roll has failed to produce a card after n-attempts, re-roll instead of using a `while` loop. Basically `if($attempts > $n) $roll = rand(0, 100)`. This works around poorly chosen random numbers, but it also can skew your distribution if it happens too often. I'll update to explain.
Jesse Dhillon
A: 

I'm going to assume for the sake of the discussion that your rarities are weighted in nice integers. (It doesn't need to be like that for the math to work. In fact, you could use actual probabilities 0 ≤ p ≤ 1 if you wanted. But it makes the explanation easier.) That is if you have a single common, uncommon, and rare card each in a deck and draw with replacement, after x+y+z draws you are expected to have drawn x common, y uncommon, and z rare cards.

You could also get the same expected draw if all cards were equally likely and your deck had x common, y uncommon, and z rare ones in it. So let's use this to find the true probability of drawing a card from each category.

Let's say that your deck has C common, U uncommon, and R rare cards in it. The equivalent deck of uniform-probability cards would have C·x common cards, U·y uncommon cards, and R·z rare cards, for a total of T = C·x + U·y + R·z cards. So the probability of selecting a common card is Pc = C·x / T, etc.

Once you've determined the category, you just have to select a card in it. They're all equally likely, so just pick one.

You can directly translate this into a program (in pseudo-PHP):

$Cx = count($common_card_array) * odds of common x
$Uy = ...
$Rz = ...
$Total = $Cx + $Uy + $Rz
$rnd = rand(1, $Total);
if ($rnd <= $Cx) {
    $rnd2 = rand(0, count($common_card_array) - 1)
    return $common_card_array[$rnd2];
}
$rnd = $rnd - $Cx;
if ($rnd <= $Uy) {
    ...
}
...
Karmastan