Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Card Game: Randomly pick 1 number out of array of 52 without duplicates

Tags:

php

shuffle

I have a simple card game (using 52 cards - no jokers) that I want to randomly pick 1 card at a time until the winning card is chosen.

I have the following array:

$cards = array(
    'diamond' => array(
        'A', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K'
    ),
    'heart' => array(
        'A', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K'
    ),
    'club' => array(
        'A', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K'
    ),
    'spades' => array(
        'A', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K'
    ),
);

As you can see, this array is sorted. I would like to shuffle them using the PHP function shuffle($cards); but it didn't work.

What can I do in order to get this suffled?

like image 762
Tech4Wilco Avatar asked Oct 25 '11 17:10

Tech4Wilco


1 Answers

I'd make classes Deck and Card. Card would hold what suit it is, as well as its 'number'. Then you can call method shuffle on Deck, which is simply an array of Cards. This way, all of the cards are sorted independent of their suit.


[Update] Features:

  • You can now specify a shuffle function. It will use PHP's shuffle if you don't specify one.
  • Restructured __construct and reset. They now use a new function, createDeck, which uses createSuit to ease the creation process.
  • Added some type checking. It's incomplete type-checking, but should help with a few crazy bugs that might happen.
  • Implements interfaces:
    • JsonSerializable. Calling jsonSerialize will return the data to be serialized. In PHP 5.4 and above you can call json_encode on the object if it implements this interface. Before then, you can call json_encode($deck->jsonSerialize()).
    • Countable. You can use count($deck) to get the size of the deck.
    • ArrayAccess. Let's you treat an object as an array.
      1. deck[0]->suit would return the suit of the first card of the deck.
      2. You can pass a Deck to any function that takes an array as input. If they use array typechecking, however, it will fail.
    • IteratorAggregate. You can use a deck in a foreach loop.

<?php

if (!interface_exists('JsonSerializable')) {
    interface JsonSerializable {
        /**
         * @return mixed Return data which should be serialized by json_encode().
         */
        function jsonSerialize();
    }
}

class Card implements JsonSerializable {
    /**
     * @var string The suit for the card
     */
    private $suit;

    /**
     * @var string The 'number' of the card.  A bit of a misnomer, A, J, Q, K can be included.
     */
    private $number;

    /**
     * Creates a new cards of suit $suit with number $number.
     * @param string $suit
     * @param string $number
     * @throws InvalidArgumentException if $suit is not a string.
     * @throws InvalidArgumentException if $number is not a string or an int.
     *
     * @todo More comprehensive checks to make sure each suit as number is valid.
     */
    public function __construct($suit, $number) {
        if (!is_string($suit)) {
            throw new InvalidArgumentException(
                'First parameter to Card::__construct() must be a string.'
            );
        }

        if (!is_string($number) && !filter_var($number, FILTER_VALIDATE_INT)) {
            throw new InvalidArgumentException(
                'Second parameter to Card::__construct() must be a string or an int.'
            );
        }
        $this->suit = $suit;
        $this->number = $number;
    }

    /**
     * @return string The suit for the card;
     */
    public function suit() {
        return $this->suit;
    }

    /**
     * @return string The number for the card;
     */
    public function number() {
        return $this->number;
    }

    /**
     * Returns a string depicting the card. Although it's json_encoded, don't
     * rely on that fact.  PHP 5.4 introduces the JsonSerializeable interface,
     * which should be used to json_encode an object.
     *
     * @return string The Card as a string.
     */
    public function __toString() {
        return json_encode($this->jsonSerialize());
    }

    /**
     * Returns the data that should be encoded  into JSON.
     * @return array Return data which should be serialized by json_encode().
     */
    public function jsonSerialize() {
        return get_object_vars($this);
    }

}

class Deck implements IteratorAggregate, ArrayAccess, Countable, JsonSerializable {

    private $deck;

    /**
     * Creates a new, unshuffled deck of cards, where the suits are in the order
     * of diamonds, hearts, clubs, spades, and each suit is ordered A, 2 .. 10,
     * J, Q, K.
     *
     * @param array $deck [optional] The deck of cards to be used.
     * @throws InvalidArgumentException if the any of the elements in $deck are not type Card.
     */
    public function __construct(array $deck=null) {
        if (isset($deck) && count($deck) > 0) {
            foreach ($deck as $card) {
                if (!($card instanceof Card)) {
                    throw new InvalidArgumentException(
                        'The first parameter to Deck::__construct must be an array'
                            . ' containing only objects of type Card'
                    );
                }
            }
            $this->deck = $deck;
        } else {
            $this->deck = $this->createDeck();
        }
    }

    /**
     * Shuffle an array.  Uses PHP's shuffle if no function is provided. If a
     * function is provided, it must take an array of Cards as its only
     * parameter.
     * @param callable $function If $function isn't callable, shuffle will be used instead
     * @return mixed Returns the result of the shuffle function.
     */
    public function shuffle($function = null) {
        if (is_callable($function, false, $callable_name)) {
            return $callable_name($this->deck);
        } else {
            return shuffle($this->deck);
        }
    }

    /**
     * Used by IteratorAggregate to loop over the object.
     * @return ArrayIterator
     */
    public function getIterator() {
        return new ArrayIterator($this->deck);
    }

    /**
     * @param string $suit The suite to create.
     * @return array The cards for the suit.
     */
    private function createSuit($suit) {
        return array(
            new Card($suit, 'A'),
            new Card($suit, '2'),
            new Card($suit, '3'),
            new Card($suit, '4'),
            new Card($suit, '5'),
            new Card($suit, '6'),
            new Card($suit, '7'),
            new Card($suit, '8'),
            new Card($suit, '9'),
            new Card($suit, '10'),
            new Card($suit, 'J'),
            new Card($suit, 'Q'),
            new Card($suit, 'K')
        );
    }

    /**
     * Returns a new, unshuffled array of cards, where the suits are in the
     * order of diamonds, hearts, clubs, spades, and each suit is ordered:
     * A, 2 .. 10, J, Q, K.
     * @return array An array of type Card.
     */
    private function createDeck() {
        return array_merge(
            $this->createSuit('diamonds'),
            $this->createSuit('hearts'),
            $this->createSuit('clubs'),
            $this->createSuit('spades')
        );
    }

    /**
     * Resets the deck to an unshuffled order, and returns the deck.
     * @return \Deck
     */
    public function reset() {
        $this->deck = $this->createDeck();
        return $this;
    }

    /**
     * Returns the data that should be encoded into JSON. Note that any objects
     * inside must also be jsonSerialized for anything less than PHP 5.4.
     *
     * @return mixed Return data which should be serialized by json_encode().
     */
    public function jsonSerialize() {
        $array = $this->deck;

        foreach($array as &$card) {
            /**
             * @var Card $card
             */
            $card = $card->jsonSerialize();
        }

        return $array;
    }

    /**
     * Used by ArrayAccess.  Determine whether an offset(index) exists.
     * @param int $index The index to test for existence.
     * @return boolean Returns true of the offset exists.
     */
    public function offsetExists($index) {
        return array_key_exists($index, $this->deck);
    }

    /**
     * Used by ArrayAccess.  Returns an item from the index provided.
     * @param int $index The index to get..
     * @return boolean Returns the object at the location.
     * @throws OutOfBoundsException if you specify an index that does not exist.
     */
    public function offsetGet($index) {
        if (!$this->offsetExists($index)) {
            throw new OutOfBoundsException(
                "The index '$index' does not exist."
            );
        }
        return $this->deck[$index];
    }

    /**
     * Used by ArrayAccess. Sets an index with the value, or adds a value if it
     * is null.
     * @param int|null $index The index to set, or null to add.
     * @param Card $value The card to set/add.
     * @return void
     * @throws InvalidArgumentException if the value provided is not a Card.
     * @throws InvalidArgumentException if the index provided is not an integer.
     * @throws OutOfBoundsException if the index provided does not exist.
     */
    public function offsetSet($index, $value) {
        if (!($value instanceof Card))
            throw new InvalidArgumentException('Decks only contain cards.');

        if ($index == null) {
            $this->deck[] = $value;
            return;
        }

        if (!is_numeric($index) || $index != (int) $index) {
            throw new InvalidArgumentException("Index '$index' must be an integer.");
        }

        if (!$this->offsetExists($index)) {
            throw new OutOfBoundsException("Index '$index' does not exist");
        }

        $this->deck[$index] = $value;
    }

    /**
     * Unsets the index location.
     * @param int $index
     * @return void
     * @throws InvalidArgumentException if the index provided does not exist.
     */
    public function offsetUnset($index) {
        if (!$this->offsetExists($index)) {
            throw new InvalidArgumentException("Index '$index' Does not exist.");
        }

        array_splice($this->deck, $index, 1);
    }

    /**
     * Returns a string depicting the card. Although it's json_encoded, don't
     * rely on that fact.  PHP 5.4 introduces the JsonSerializeable interface,
     * which should be used to json_encode an object.
     *
     * @return string The Card as a string.
     */
    public function __toString() {
        return json_encode($this->jsonSerialize());
    }

    /**
     * Used by interface Count.
     * @return int The size of the deck.
     */
    function count() {
        return count($this->deck);
    }

}

header('Content-type:text/plain');

$deck = new Deck();

echo "Original Deck:\n";


foreach ($deck as $card) {
    echo $card . "\n";
}

$deck->shuffle();

echo "After Shuffle:\n";

foreach ($deck as $card) {
    echo $card . "\n";
}
/**
 * Shuffles the array using the Fisher-Yates algorithm.
 */
function fisherYatesShuffle(array &$items) {
    for ($i = count($items) - 1; $i > 0; $i--) {
        $j = @mt_rand(0, $i);
        $tmp = $items[$i];
        $items[$i] = $items[$j];
        $items[$j] = $tmp;
    }
}

$deck->shuffle('fisherYatesShuffle');


echo "Reset, then custom shuffle:\n";

foreach ($deck as $card) {
    echo $card . "\n";
}

echo "First card in the deck:";
echo $deck['0'] . "\n";

echo "Deck reset. __toString() on \$deck: ".$deck->reset()."\n";
like image 93
Levi Morrison Avatar answered Nov 15 '22 15:11

Levi Morrison