This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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 | /** |
||
173 | * Group cards by faces |
||
174 | * |
||
175 | * @param array $cards |
||
176 | * @return array |
||
177 | */ |
||
178 | private function findMultipleFaceCards(array $cards) |
||
179 | { |
||
180 | $faces = [ |
||
181 | 'A' => [], |
||
182 | 'K' => [], |
||
183 | 'Q' => [], |
||
184 | 'J' => [], |
||
185 | '10' => [], |
||
186 | '9' => [], |
||
187 | '8' => [], |
||
188 | '7' => [], |
||
189 | '6' => [], |
||
190 | '5' => [], |
||
191 | '4' => [], |
||
192 | '3' => [], |
||
193 | '2' => [], |
||
194 | ]; |
||
195 | |||
196 | foreach ($cards as $card) { |
||
197 | $cardFace = substr($card, 0, 1); |
||
198 | |||
199 | if (substr($card, 0, 2) == 10) { |
||
200 | $cardFace = substr($card, 0, 2); |
||
201 | } |
||
202 | |||
203 | switch ($cardFace) { |
||
204 | case 'A': |
||
205 | $faces['A'][] = $card; |
||
206 | break; |
||
207 | case 'K': |
||
208 | $faces['K'][] = $card; |
||
209 | break; |
||
210 | case 'Q': |
||
211 | $faces['Q'][] = $card; |
||
212 | break; |
||
213 | case 'J': |
||
214 | $faces['J'][] = $card; |
||
215 | break; |
||
216 | case '10': |
||
217 | $faces['10'][] = $card; |
||
218 | break; |
||
219 | case '9': |
||
220 | $faces['9'][] = $card; |
||
221 | break; |
||
222 | case '8': |
||
223 | $faces['8'][] = $card; |
||
224 | break; |
||
225 | case '7': |
||
226 | $faces['7'][] = $card; |
||
227 | break; |
||
228 | case '6': |
||
229 | $faces['6'][] = $card; |
||
230 | break; |
||
231 | case '5': |
||
232 | $faces['5'][] = $card; |
||
233 | break; |
||
234 | case '4': |
||
235 | $faces['4'][] = $card; |
||
236 | break; |
||
237 | case '3': |
||
238 | $faces['3'][] = $card; |
||
239 | break; |
||
240 | case '2': |
||
241 | $faces['2'][] = $card; |
||
242 | break; |
||
243 | default: |
||
244 | throw new \Exception(sprintf("The face %s doesn't exist!", $color)); |
||
245 | } |
||
246 | } |
||
247 | |||
248 | return $faces; |
||
249 | } |
||
250 | |||
251 | /** |
||
252 | * Return true if the cards are a Royal Flush. |
||
253 | * |
||
254 | * @param array $cards |
||
255 | * |
||
256 | * @return bool |
||
257 | */ |
||
258 | private function isRoyalFlush(array $cards) |
||
259 | { |
||
260 | $hearts = [ |
||
261 | ['AH', 'KH', 'QH', 'JH', '10H'], |
||
262 | ['AD', 'KD', 'QD', 'JD', '10D'], |
||
263 | ['AS', 'KS', 'QS', 'JS', '10S'], |
||
264 | ['AC', 'KC', 'QC', 'JC', '10C'], |
||
265 | ]; |
||
266 | |||
267 | foreach ($hearts as $value) { |
||
268 | if (count(array_intersect($value, $cards)) === 5) { |
||
269 | return $this->getResponse('Royal Flush', 10, $this->getRank($value), $value); |
||
270 | } |
||
271 | } |
||
272 | |||
273 | return false; |
||
274 | } |
||
275 | |||
276 | /** |
||
277 | * Return an array if the cards are a Straight Flush |
||
278 | * otherwise return false. |
||
279 | * |
||
280 | * @param array $cards |
||
281 | * |
||
282 | * @return array|bool |
||
283 | */ |
||
284 | private function isStraightFlush(array $cards) |
||
285 | { |
||
286 | // Check if Flush first, because isStraight() remove duplicate cards |
||
287 | if ($straightFlushCards = $this->isFlush($cards)) { |
||
288 | if ($straightCards = $this->isStraight($straightFlushCards['cards'])) { |
||
289 | return $this->getResponse('Straight Flush', 9, $straightCards['card_rank'], $straightCards['cards']); |
||
290 | } |
||
291 | } |
||
292 | |||
293 | return false; |
||
294 | } |
||
295 | |||
296 | /** |
||
297 | * Return an array if the cards are a Four of a Kind |
||
298 | * otherwise return false. |
||
299 | * |
||
300 | * @example AC AD AS AH 2D 7D 10S |
||
301 | * |
||
302 | * @param array $cards |
||
303 | * |
||
304 | * @return array|bool |
||
305 | */ |
||
306 | View Code Duplication | private function isFourOfAKind(array $cards) |
|
0 ignored issues
–
show
|
|||
307 | { |
||
308 | $faces = $this->findMultipleFaceCards($cards); |
||
309 | |||
310 | foreach ($faces as $face => $groupedFaces) { |
||
311 | if (count($groupedFaces) == 4) { |
||
312 | return $this->getResponse('Four of a kind', 8, $this->getRank($groupedFaces), $groupedFaces); |
||
313 | } |
||
314 | } |
||
315 | |||
316 | return false; |
||
317 | } |
||
318 | |||
319 | /** |
||
320 | * Return an array if the cards are a Full House |
||
321 | * otherwise return false. |
||
322 | * |
||
323 | * @param array $cards |
||
324 | * |
||
325 | * @return array|bool |
||
326 | */ |
||
327 | private function isFullHouse(array $cards) |
||
328 | { |
||
329 | $faces = $this->findMultipleFaceCards($cards); |
||
330 | $res = []; |
||
331 | |||
332 | foreach ($faces as $face => $groupedFaces) { |
||
333 | $nbCards = count($groupedFaces); |
||
334 | |||
335 | if ($nbCards == 3) { |
||
336 | foreach ($groupedFaces as $value) { |
||
337 | $res[] = $value; |
||
338 | } |
||
339 | unset($faces[$face]); |
||
340 | break; |
||
341 | } |
||
342 | } |
||
343 | |||
344 | foreach ($faces as $face => $groupedFaces) { |
||
345 | $nbCards = count($groupedFaces); |
||
346 | |||
347 | if ($nbCards >= 2) { |
||
348 | foreach ($groupedFaces as $key => $value) { |
||
349 | $res[] = $value; |
||
350 | //We just pick up just 2 cards if there is more |
||
351 | if ($nbCards > 2 && $key == 1) { |
||
352 | break; |
||
353 | } |
||
354 | } |
||
355 | unset($faces[$face]); |
||
356 | break; |
||
357 | } |
||
358 | } |
||
359 | |||
360 | if (count($res) == 5) { |
||
361 | return $this->getResponse('Full House', 7, $this->getRank($res), $res); |
||
362 | } |
||
363 | |||
364 | return false; |
||
365 | } |
||
366 | |||
367 | /** |
||
368 | * Return true if the cards are a Flush. |
||
369 | * |
||
370 | * @param array $cards |
||
371 | * |
||
372 | * @return bool |
||
373 | */ |
||
374 | private function isFlush(array $cards) |
||
375 | { |
||
376 | $colors = [ |
||
377 | 'S' => [], |
||
378 | 'D' => [], |
||
379 | 'C' => [], |
||
380 | 'H' => [], |
||
381 | ]; |
||
382 | |||
383 | foreach ($cards as $card) { |
||
384 | $color = substr($card, -1); |
||
385 | |||
386 | if (!in_array($color, ['S', 'D', 'C', 'H'])) { |
||
387 | throw new \Exception(sprintf("The color %s doesn't exist!", $color)); |
||
388 | } |
||
389 | |||
390 | if ($color == 'S') { |
||
391 | $colors['S'][] = $card; |
||
392 | } elseif ($color == 'D') { |
||
393 | $colors['D'][] = $card; |
||
394 | } elseif ($color == 'C') { |
||
395 | $colors['C'][] = $card; |
||
396 | } elseif ($color == 'H') { |
||
397 | $colors['H'][] = $card; |
||
398 | } |
||
399 | } |
||
400 | |||
401 | foreach ($colors as $color => $groupedCards) { |
||
402 | if (count($groupedCards) == 5) { |
||
403 | $flushCards = $this->sortCards($groupedCards); |
||
404 | |||
405 | return $this->getResponse('Flush', 6, $this->getRank($flushCards), $flushCards); |
||
406 | } |
||
407 | } |
||
408 | |||
409 | return false; |
||
410 | } |
||
411 | |||
412 | /** |
||
413 | * Return false if the cards are not a Straight |
||
414 | * otherwise it returns an array. |
||
415 | * |
||
416 | * @todo Straight from bottom, i.e. AC 2D 3H 4H 5S |
||
417 | * |
||
418 | * @param array $cards |
||
419 | * |
||
420 | * @return array|bool |
||
421 | */ |
||
422 | private function isStraight(array $cards) |
||
423 | { |
||
424 | $cards = $this->sortCards($cards); |
||
425 | $response = []; |
||
426 | |||
427 | // Special straight from bottom with Ace |
||
428 | if (array_key_exists(1, $cards) && |
||
429 | array_key_exists(10, $cards) && |
||
430 | array_key_exists(11, $cards) && |
||
431 | array_key_exists(12, $cards) && |
||
432 | array_key_exists(13, $cards) |
||
433 | ) { |
||
434 | foreach ($cards as $key => $card) { |
||
435 | if ($key == 1 || $key == 10 || $key == 11 || $key == 12 || $key == 13) { |
||
436 | $response[] = $card; |
||
437 | } |
||
438 | } |
||
439 | |||
440 | return $this->getResponse('Straight', 5, 6, $response); |
||
441 | } |
||
442 | |||
443 | foreach ($cards as $key => $value) { |
||
444 | if (array_key_exists($key + 1, $cards) || (array_key_exists($key - 1, $cards) && count($response) == 4)) { |
||
445 | $response[$key] = $value; |
||
446 | } else { |
||
447 | $response = []; |
||
448 | } |
||
449 | |||
450 | if (count($response) == 5) { |
||
451 | return $this->getResponse('Straight', 5, $this->getRank($response), $response); |
||
452 | } |
||
453 | } |
||
454 | |||
455 | return false; |
||
456 | } |
||
457 | |||
458 | /** |
||
459 | * Return an array if it's a Tree of a Kind or false if not. |
||
460 | * |
||
461 | * @param array $cards |
||
462 | * |
||
463 | * @return array|bool |
||
464 | */ |
||
465 | View Code Duplication | private function isTreeOfAKind(array $cards) |
|
0 ignored issues
–
show
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. ![]() |
|||
466 | { |
||
467 | $faces = $this->findMultipleFaceCards($cards); |
||
468 | |||
469 | foreach ($faces as $face => $groupedFaces) { |
||
470 | if (count($groupedFaces) == 3) { |
||
471 | return $this->getResponse('Three of a kind', 4, $this->getRank($groupedFaces), $groupedFaces); |
||
472 | } |
||
473 | } |
||
474 | |||
475 | return false; |
||
476 | } |
||
477 | |||
478 | /** |
||
479 | * Find one or n pair |
||
480 | * |
||
481 | * @param string $typeOfPair |
||
482 | * @param int $nbCards |
||
483 | * @param array $cards |
||
484 | * |
||
485 | * @return array|bool |
||
486 | */ |
||
487 | private function findPair($typeOfPair, $handRank, $nbCards, array $cards) |
||
488 | { |
||
489 | $faces = $this->findMultipleFaceCards($cards); |
||
490 | $response = []; |
||
491 | |||
492 | foreach ($faces as $face => $groupedFaces) { |
||
493 | if (count($groupedFaces) == 2 && count($response) !== $nbCards) { |
||
494 | foreach ($groupedFaces as $value) { |
||
495 | $response[] = $value; |
||
496 | } |
||
497 | } |
||
498 | } |
||
499 | |||
500 | if (count($response) == $nbCards) { |
||
501 | return $this->getResponse($typeOfPair, $handRank, $this->getRank($response), $response); |
||
502 | } |
||
503 | |||
504 | return false; |
||
505 | } |
||
506 | |||
507 | /** |
||
508 | * Return an array if it's Two Pairs or false if not. |
||
509 | * |
||
510 | * @param array $cards |
||
511 | * |
||
512 | * @return array|bool |
||
513 | */ |
||
514 | private function isTwoPairs(array $cards) |
||
515 | { |
||
516 | if ($response = $this->findPair('Two Pairs', 3, 4, $cards)) { |
||
517 | return $response; |
||
518 | } |
||
519 | |||
520 | return false; |
||
521 | } |
||
522 | |||
523 | /** |
||
524 | * Return an array if it's One Pair or false if not. |
||
525 | * |
||
526 | * @param array $cards |
||
527 | * |
||
528 | * @return array|bool |
||
529 | */ |
||
530 | private function isOnePair(array $cards) |
||
531 | { |
||
532 | if ($response = $this->findPair('One Pair', 2, 2, $cards)) { |
||
533 | return $response; |
||
534 | } |
||
535 | |||
536 | return false; |
||
537 | } |
||
538 | |||
539 | /** |
||
540 | * Return an array if it's High Card or false if not. |
||
541 | * |
||
542 | * @param array $cards |
||
543 | * |
||
544 | * @return array|bool |
||
545 | */ |
||
546 | private function isHighCard(array $cards) |
||
547 | { |
||
548 | $response = []; |
||
549 | $response[] = current($cards); |
||
550 | |||
551 | return $this->getResponse('High card', 1, $this->getRank($response), $response); |
||
552 | } |
||
553 | } |
||
554 |
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.