Completed
Push — master ( f17f45...f16efc )
by Pierre-Henri
02:09
created

HandFinder::isRoyalFlush()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 17
rs 9.4285
cc 3
eloc 10
nc 3
nop 1
1
<?php
2
3
namespace Bourdeau\Bundle\HandEvaluatorBundle\HandEvaluator;
4
5
use Bourdeau\Bundle\HandEvaluatorBundle\HandEvaluator\CardValidator;
6
7
/**
8
 * Hand Evaluator for Poker
9
 *
10
 * @author Pierre-Henri Bourdeau <[email protected]>
11
 */
12
class HandFinder
13
{
14
    private $cardValidator;
15
16
    /**
17
     * Constructor
18
     *
19
     * @param CardValidator $cardValidator [description]
20
     */
21
    public function __construct(CardValidator $cardValidator)
22
    {
23
        $this->cardValidator = $cardValidator;
24
    }
25
26
    /**
27
     *
28
     * Will return the best hand with the given cards.
29
     *
30
     * IMPORTANT: the oder in wich the methods are called is critical.
31
     * Changing it will break the code, as a Three of a Kind will always
32
     * be found in a Full House for instance.
33
     *
34
     * @param array $cards
35
     *
36
     * @return string
37
     */
38
    public function findHand(array $cards)
39
    {
40
        if (!$this->cardValidator->areValid($cards)) {
41
            throw new \Exception('Your cards are not valid');
42
        }
43
44
        if ($res = $this->isRoyalFlush($cards)) {
45
            return $res;
46
        }
47
        if ($res = $this->isStraightFlush($cards)) {
48
            return $res;
49
        }
50
        if ($res = $this->isFourOfAKind($cards)) {
51
            return $res;
52
        }
53
        if ($res = $this->isFullHouse($cards)) {
54
            return $res;
55
        }
56
        if ($res = $this->isFlush($cards)) {
57
            return $res;
58
        }
59
        if ($res = $this->isStraight($cards)) {
60
            return $res;
61
        }
62
        if ($res = $this->isTreeOfAKind($cards)) {
63
            return $res;
64
        }
65
        if ($res = $this->isTwoPairs($cards)) {
66
            return $res;
67
        }
68
        if ($res = $this->isOnePair($cards)) {
69
            return $res;
70
        }
71
        if ($res = $this->isHighCard($cards)) {
72
            return $res;
73
        }
74
75
        throw new \Exception("Couldn't find a Hand!");
76
    }
77
78
    /**
79
     * Sort cards
80
     *
81
     * @param  array  $cards
82
     *
83
     * @return array
84
     */
85
    private function sortCards(array $cards)
86
    {
87
        $data = [];
88
        $cardOrder = [
89
            'A'  => 1,
90
            'K'  => 2,
91
            'Q'  => 3,
92
            'J'  => 4,
93
            '10' => 5,
94
            '9'  => 6,
95
            '8'  => 7,
96
            '7'  => 8,
97
            '6'  => 9,
98
            '5'  => 10,
99
            '4'  => 11,
100
            '3'  => 12,
101
            '2'  => 13,
102
        ];
103
104
        foreach ($cards as $card) {
105
            $cardKey = substr($card, 0, -1);
106
            if (array_key_exists($cardKey, $cardOrder)) {
107
                $data[$cardOrder[$cardKey]] = $card;
108
            }
109
        }
110
111
        ksort($data);
112
113
        return $data;
114
    }
115
116
    /**
117
     * Get rank of a card
118
     *
119
     * @param  array  $cards
120
     *
121
     * @return int
122
     */
123
    private function getRank(array $cards)
124
    {
125
        $cardRank = [
126
            'A'  => 13,
127
            'K'  => 12,
128
            'Q'  => 11,
129
            'J'  => 10,
130
            '10' => 9,
131
            '9'  => 8,
132
            '8'  => 7,
133
            '7'  => 6,
134
            '6'  => 5,
135
            '5'  => 4,
136
            '4'  => 3,
137
            '3'  => 2,
138
            '2'  => 1,
139
        ];
140
141
        $card = current($cards);
142
143
        $cardFace = substr($card, 0, -1);
144
145
        if (array_key_exists($cardFace, $cardRank)) {
146
            return $cardRank[$cardFace];
147
        }
148
149
        throw new \Exception(sprintf('No rank found for card %s'), $card);
150
    }
151
152
    /**
153
     * Return a formated response.
154
     *
155
     * @param string $handName
156
     * @param int    $handRank
157
     * @param int    $cardRank
158
     * @param array  $response
159
     *
160
     * @return array
161
     */
162
    private function getResponse($handName, $handRank, $cardRank, array $response)
163
    {
164
        return [
165
            'hand_name' => $handName,
166
            'hand_rank' => (int) $handRank,
167
            'card_rank' => (int) $cardRank,
168
            'cards'     => $response,
169
        ];
170
    }
171
172
    private function findMultipleFaceCards(array $cards)
173
    {
174
        $faces = [
175
            'A' => [],
176
            'K' => [],
177
            'Q' => [],
178
            'J' => [],
179
            '10' => [],
180
            '9' => [],
181
            '8' => [],
182
            '7' => [],
183
            '6' => [],
184
            '5' => [],
185
            '4' => [],
186
            '3' => [],
187
            '2' => [],
188
        ];
189
190
        foreach ($cards as $card) {
191
            $cardFace = substr($card, 0, 1);
192
193
            if (substr($card, 0, 2) == 10) {
194
                $cardFace = substr($card, 0, 2);
195
            }
196
197
            switch ($cardFace) {
198
                case 'A':
199
                    $faces['A'][] = $card;
200
                    break;
201
                case 'K':
202
                    $faces['K'][] = $card;
203
                    break;
204
                case 'Q':
205
                    $faces['Q'][] = $card;
206
                    break;
207
                case 'J':
208
                    $faces['J'][] = $card;
209
                    break;
210
                case '10':
211
                    $faces['10'][] = $card;
212
                    break;
213
                case '9':
214
                    $faces['9'][] = $card;
215
                    break;
216
                case '8':
217
                    $faces['8'][] = $card;
218
                    break;
219
                case '7':
220
                    $faces['7'][] = $card;
221
                    break;
222
                case '6':
223
                    $faces['6'][] = $card;
224
                    break;
225
                case '5':
226
                    $faces['5'][] = $card;
227
                    break;
228
                case '4':
229
                    $faces['4'][] = $card;
230
                    break;
231
                case '3':
232
                    $faces['3'][] = $card;
233
                    break;
234
                case '2':
235
                    $faces['2'][] = $card;
236
                    break;
237
                default:
238
                    throw new \Exception(sprintf("The face %s doesn't exist!", $color));
239
            }
240
        }
241
242
        return $faces;
243
    }
244
    /**
245
     * Return true if the cards are a Royal Flush.
246
     *
247
     * @param array $cards
248
     *
249
     * @return bool
250
     */
251
    private function isRoyalFlush(array $cards)
252
    {
253
        $hearts = [
254
            ['AH', 'KH', 'QH', 'JH', '10H'],
255
            ['AD', 'KD', 'QD', 'JD', '10D'],
256
            ['AS', 'KS', 'QS', 'JS', '10S'],
257
            ['AC', 'KC', 'QC', 'JC', '10C'],
258
        ];
259
260
        foreach ($hearts as $value) {
261
            if (count(array_intersect($value, $cards)) === 5) {
262
                return $this->getResponse('Royal Flush', 10, $this->getRank($value), $value);
263
            }
264
        }
265
266
        return false;
267
    }
268
269
    /**
270
     * Return an array if the cards are a Straight Flush
271
     * otherwise return false.
272
     *
273
     * @param array $cards
274
     *
275
     * @return array|bool
276
     */
277
    private function isStraightFlush(array $cards)
278
    {
279
        // Check if Flush first, because isStraight() remove duplicate cards
280
        if ($straightFlushCards = $this->isFlush($cards)) {
281
            if ($straightCards = $this->isStraight($straightFlushCards['cards'])) {
282
                return $this->getResponse('Straight Flush', 9, $straightCards['card_rank'], $straightCards['cards']);
283
            }
284
        }
285
286
        return false;
287
    }
288
289
    /**
290
     * Return an array if the cards are a Four of a Kind
291
     * otherwise return false.
292
     *
293
     * @example AC AD AS AH 2D 7D 10S
294
     *
295
     * @param array $cards
296
     *
297
     * @return array|bool
298
     */
299 View Code Duplication
    private function isFourOfAKind(array $cards)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
300
    {
301
        $faces = $this->findMultipleFaceCards($cards);
302
303
        foreach ($faces as $face => $groupedFaces) {
304
            if (count($groupedFaces) == 4) {
305
                return $this->getResponse('Four of a kind', 8, $this->getRank($groupedFaces), $groupedFaces);
306
            }
307
        }
308
309
        return false;
310
    }
311
312
    /**
313
     * Return an array if the cards are a Full House
314
     * otherwise return false.
315
     *
316
     * @param array $cards
317
     *
318
     * @return array|bool
319
     */
320
    private function isFullHouse(array $cards)
321
    {
322
        $faces = $this->findMultipleFaceCards($cards);
323
        $res = [];
324
325
        foreach ($faces as $face => $groupedFaces) {
326
            $nbCards = count($groupedFaces);
327
328
            if ($nbCards == 3) {
329
                foreach ($groupedFaces as $value) {
330
                    $res[] = $value;
331
                }
332
                unset($faces[$face]);
333
                break;
334
            }
335
        }
336
337
        foreach ($faces as $face => $groupedFaces) {
338
            $nbCards = count($groupedFaces);
339
340
            if ($nbCards >= 2) {
341
                foreach ($groupedFaces as $key => $value) {
342
                    $res[] = $value;
343
                    //We just pick up just 2 cards if there is more
344
                    if ($nbCards > 2 && $key == 1) {
345
                        break;
346
                    }
347
                }
348
                unset($faces[$face]);
349
                break;
350
            }
351
        }
352
353
        if (count($res) == 5) {
354
            return $this->getResponse('Full House', 7, $this->getRank($res), $res);
355
        }
356
357
        return false;
358
    }
359
360
    /**
361
     * Return true if the cards are a Flush.
362
     *
363
     * @param array $cards
364
     *
365
     * @return bool
366
     */
367
    private function isFlush(array $cards)
368
    {
369
        $colors = [
370
            'S' => [],
371
            'D' => [],
372
            'C' => [],
373
            'H' => [],
374
        ];
375
376
        foreach ($cards as $card) {
377
            $color = substr($card, -1);
378
379
            if (!in_array($color, ['S', 'D', 'C', 'H'])) {
380
                throw new \Exception(sprintf("The color %s doesn't exist!", $color));
381
            }
382
383
            if ($color == 'S') {
384
                $colors['S'][] = $card;
385
            } elseif ($color == 'D') {
386
                $colors['D'][] = $card;
387
            } elseif ($color == 'C') {
388
                $colors['C'][] = $card;
389
            } elseif ($color == 'H') {
390
                $colors['H'][] = $card;
391
            }
392
        }
393
394
        foreach ($colors as $color => $groupedCards) {
395
            if (count($groupedCards) == 5) {
396
                $flushCards = $this->sortCards($groupedCards);
397
398
                return $this->getResponse('Flush', 6, $this->getRank($flushCards), $flushCards);
399
            }
400
        }
401
402
        return false;
403
    }
404
405
    /**
406
     * Return false if the cards are not a Straight
407
     * otherwise it returns an array.
408
     *
409
     * @todo Straight from bottom, i.e. AC 2D 3H 4H 5S
410
     *
411
     * @param array $cards
412
     *
413
     * @return array|bool
414
     */
415
    private function isStraight(array $cards)
416
    {
417
        $cards = $this->sortCards($cards);
418
        $response = [];
419
420
        // Special straight from bottom with Ace
421
        if (array_key_exists(1, $cards) &&
422
            array_key_exists(10, $cards) &&
423
            array_key_exists(11, $cards) &&
424
            array_key_exists(12, $cards) &&
425
            array_key_exists(13, $cards)
426
        ) {
427
            foreach ($cards as $key => $card) {
428
                if ($key == 1 || $key == 10 || $key == 11 || $key == 12 || $key == 13) {
429
                    $response[] = $card;
430
                }
431
            }
432
433
            return $this->getResponse('Straight', 5, 6, $response);
434
        }
435
436
        foreach ($cards as $key => $value) {
437
            if (array_key_exists($key + 1, $cards) || (array_key_exists($key - 1, $cards) && count($response) == 4)) {
438
                $response[$key] = $value;
439
            } else {
440
                $response = [];
441
            }
442
443
            if (count($response) == 5) {
444
                return $this->getResponse('Straight', 5, $this->getRank($response), $response);
445
            }
446
        }
447
448
        return false;
449
    }
450
451
    /**
452
     * Return an array if it's a Tree of a Kind or false if not.
453
     *
454
     * @param array $cards
455
     *
456
     * @return array|bool
457
     */
458 View Code Duplication
    private function isTreeOfAKind(array $cards)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
459
    {
460
        $faces = $this->findMultipleFaceCards($cards);
461
462
        foreach ($faces as $face => $groupedFaces) {
463
            if (count($groupedFaces) == 3) {
464
                return $this->getResponse('Three of a kind', 4, $this->getRank($groupedFaces), $groupedFaces);
465
            }
466
        }
467
468
        return false;
469
    }
470
471
    /**
472
     * Find one or n pair
473
     *
474
     * @param  string $typeOfPair
475
     * @param  int $nbCards
476
     * @param  array  $cards
477
     *
478
     * @return array|bool
479
     */
480
    private function findPair($typeOfPair, $handRank, $nbCards, array $cards)
481
    {
482
        $faces = $this->findMultipleFaceCards($cards);
483
        $response = [];
484
485
        foreach ($faces as $face => $groupedFaces) {
486
            if (count($groupedFaces) == 2 && count($response) !== $nbCards) {
487
                foreach ($groupedFaces as $value) {
488
                    $response[] = $value;
489
                }
490
            }
491
        }
492
493
        if (count($response) == $nbCards) {
494
            return $this->getResponse($typeOfPair, $handRank, $this->getRank($response), $response);
495
        }
496
497
        return false;
498
    }
499
500
    /**
501
     * Return an array if it's Two Pairs or false if not.
502
     *
503
     * @param array $cards
504
     *
505
     * @return array|bool
506
     */
507
    private function isTwoPairs(array $cards)
508
    {
509
        if ($response = $this->findPair('Two Pairs', 3, 4, $cards)) {
510
            return $response;
511
        }
512
513
        return false;
514
    }
515
516
    /**
517
     * Return an array if it's One Pair or false if not.
518
     *
519
     * @param array $cards
520
     *
521
     * @return array|bool
522
     */
523
    private function isOnePair(array $cards)
524
    {
525
        if ($response = $this->findPair('One Pair', 2, 2, $cards)) {
526
            return $response;
527
        }
528
529
        return false;
530
    }
531
532
    /**
533
     * Return an array if it's High Card or false if not.
534
     *
535
     * @param array $cards
536
     *
537
     * @return array|bool
538
     */
539
    private function isHighCard(array $cards)
540
    {
541
        $response = [];
542
        $response[] =  current($cards);
543
544
        return $this->getResponse('High card', 1, $this->getRank($response), $response);
545
    }
546
}
547