Completed
Push — master ( 8fe4e7...fafc98 )
by Dan
23:10
created

Round::id()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Cysha\Casino\Holdem\Game;
4
5
use Cysha\Casino\Cards\Contracts\CardResults;
6
use Cysha\Casino\Game\Chips;
7
use Cysha\Casino\Game\ChipStackCollection;
8
use Cysha\Casino\Game\Contracts\Dealer as DealerContract;
9
use Cysha\Casino\Game\Contracts\GameParameters;
10
use Cysha\Casino\Game\Contracts\Player as PlayerContract;
11
use Cysha\Casino\Game\PlayerCollection;
12
use Cysha\Casino\Holdem\Exceptions\RoundException;
13
use Cysha\Casino\Holdem\Game\LeftToAct;
14
use Ramsey\Uuid\Uuid;
15
16
class Round
17
{
18
    /**
19
     * @var Uuid
20
     */
21
    private $id;
22
23
    /**
24
     * @var Table
25
     */
26
    private $table;
27
28
    /**
29
     * @var ChipStackCollection
30
     */
31
    private $betStacks;
32
33
    /**
34
     * @var PlayerCollection
35
     */
36
    private $foldedPlayers;
37
38
    /**
39
     * @var ChipPotCollection
40
     */
41
    private $chipPots;
42
43
    /**
44
     * @var ChipPot
45
     */
46
    private $currentPot;
47
48
    /**
49
     * @var ActionCollection
50
     */
51
    private $playerActions;
52
53
    /**
54
     * @var PlayerCollection
55
     */
56
    private $leftToAct;
57
58
    /**
59
     * @var GameParameters
60
     */
61
    private $gameRules;
62
63
    /**
64
     * Round constructor.
65
     *
66
     * @param Uuid $id
67
     * @param Table $table
68
     * @param GameParameters $gameRules
69
     */
70
    private function __construct(Uuid $id, Table $table, GameParameters $gameRules)
71
    {
72
        $this->id = $id;
73
        $this->table = $table;
74
        $this->chipPots = ChipPotCollection::make();
75
        $this->currentPot = ChipPot::create();
76
        $this->betStacks = ChipStackCollection::make();
77 50
        $this->foldedPlayers = PlayerCollection::make();
78
        $this->playerActions = ActionCollection::make();
79 50
        $this->leftToAct = LeftToAct::make();
80 50
        $this->gameRules = $gameRules;
81 50
82 50
        // shuffle the deck ready
83 50
        $this->dealer()->shuffleDeck();
84 50
85 50
        // add the default pot to the chipPots
86 50
        $this->chipPots->push($this->currentPot);
87 50
88 50
        // init the betStacks and actions for each player
89
        $this->resetBetStacks();
90
        $this->setupLeftToAct();
91 50
    }
92
93
    /**
94 50
     * Start a Round of poker.
95
     *
96
     * @param Uuid $id
97 50
     * @param Table $table
98 50
     * @param GameParameters $gameRules
99 50
     *
100
     * @return Round
101
     */
102
    public static function start(Uuid $id, Table $table, GameParameters $gameRules): Round
103
    {
104
        return new static($id, $table, $gameRules);
105
    }
106
107
    /**
108 50
     * Run the cleanup procedure for an end of Round.
109
     */
110 50
    public function end()
111
    {
112
        $this->dealer()->checkCommunityCards();
113
114
        $this->collectChipTotal();
115
116 11
        $this->distributeWinnings();
117
118
        $this->table()->moveButton();
119
    }
120 11
121
    /**
122 11
     * @return Uuid
123
     */
124 11
    public function id(): Uuid
125 11
    {
126
        return $this->id;
127
    }
128
129
    /**
130 50
     * @return DealerContract
131
     */
132 50
    public function dealer(): DealerContract
133
    {
134
        return $this->table->dealer();
135
    }
136
137
    /**
138 3
     * @return PlayerCollection
139
     */
140 3
    public function players(): PlayerCollection
141
    {
142
        return $this->table->players();
143
    }
144
145
    /**
146 12
     * @return PlayerCollection
147
     */
148 12
    public function playersStillIn(): PlayerCollection
149
    {
150
        return $this->table->playersSatDown()->diff($this->foldedPlayers());
151
    }
152
153
    /**
154 19
     * @return PlayerCollection
155
     */
156 19
    public function foldedPlayers(): PlayerCollection
157
    {
158
        return $this->foldedPlayers;
159
    }
160
161
    /**
162 1
     * @return ActionCollection
163
     */
164 1
    public function playerActions(): ActionCollection
165
    {
166
        return $this->playerActions;
167
    }
168
169
    /**
170 35
     * @return LeftToAct
171
     */
172 35
    public function leftToAct(): LeftToAct
173
    {
174
        return $this->leftToAct;
175
    }
176
177
    /**
178 36
     * @return Table
179
     */
180 36
    public function table(): Table
181
    {
182
        return $this->table;
183
    }
184
185
    /**
186 50
     * @return ChipStackCollection
187
     */
188 50
    public function betStacks(): ChipStackCollection
189
    {
190
        return $this->betStacks;
191
    }
192
193
    /**
194 13
     * @return GameParameters
195
     */
196 13
    public function gameRules(): GameParameters
197
    {
198
        return $this->gameRules;
199
    }
200
201
    /**
202 29
     * @return int
203
     */
204 29
    public function betStacksTotal(): int
205
    {
206
        return $this->betStacks()->total()->amount();
207
    }
208
209
    public function dealHands()
210 22
    {
211
        $players = $this->table()
212 22
            ->playersSatDown()
213 22
            ->resetPlayerListFromSeat($this->table()->button() + 1);
214
215
        $this->dealer()->dealHands($players);
216
    }
217
218 2
    /**
219
     * Runs over each chipPot and assigns the chips to the winning player.
220 2
     */
221
    private function distributeWinnings()
222
    {
223
        $this->chipPots()
224
            ->reverse()
225
            ->each(function (ChipPot $chipPot) {
226 11
                // if only 1 player participated to pot, he wins it no arguments
227
                if ($chipPot->players()->count() === 1) {
228 11
                    $potTotal = $chipPot->chips()->total();
229 11
230
                    $chipPot->players()->first()->chipStack()->add($potTotal);
231
232 11
                    $this->chipPots()->remove($chipPot);
233 6
234
                    return;
235 6
                }
236
237 6
                $activePlayers = $chipPot->players()->diff($this->foldedPlayers());
238
239 6
                $playerHands = $this->dealer()->hands()->findByPlayers($activePlayers);
240
                $evaluate = $this->dealer()->evaluateHands($this->dealer()->communityCards(), $playerHands);
241
242 11
                // if just 1, the player with that hand wins
243
                if ($evaluate->count() === 1) {
244 11
                    $player = $evaluate->first()->hand()->player();
245 11
                    $potTotal = $chipPot->chips()->total();
246
247
                    $player->chipStack()->add($potTotal);
248 11
249 11
                    $this->chipPots()->remove($chipPot);
250 11
                } else {
251
                    // if > 1 hand is evaluated as highest, split the pot evenly between the players
252 11
253
                    $potTotal = $chipPot->chips()->total();
254 11
255
                    // split the pot between the number of players
256
                    $splitTotal = Chips::fromAmount(($potTotal->amount() / $evaluate->count()));
257
                    $evaluate->each(function (CardResults $result) use ($splitTotal) {
258
                        $result->hand()->player()->chipStack()->add($splitTotal);
259 1
                    });
260
261
                    $this->chipPots()->remove($chipPot);
262 1
                }
263
            })
264 1
        ;
265 1
    }
266
267 1
    /**
268
     * @param Player $actualPlayer
269 11
     *
270
     * @return bool
271 11
     */
272
    public function playerIsStillIn(PlayerContract $actualPlayer)
273
    {
274
        $playerCount = $this->playersStillIn()->filter->equals($actualPlayer)->count();
275
276
        return $playerCount === 1;
277
    }
278 1
279
    /**
280 1
     * @return PlayerContract
281
     */
282 1
    public function playerWithButton(): PlayerContract
283
    {
284
        return $this->table()->locatePlayerWithButton();
285
    }
286
287
    /**
288 7
     * @return PlayerContract
289
     */
290 7
    public function playerWithSmallBlind(): PlayerContract
291
    {
292
        if ($this->table()->playersSatDown()->count() === 2) {
293
            return $this->table()->playersSatDown()->get(0);
294
        }
295
296 21
        return $this->table()->playersSatDown()->get($this->table()->button() + 1);
297
    }
298 21
299 5
    /**
300
     * @return PlayerContract
301
     */
302 16
    public function playerWithBigBlind(): PlayerContract
303
    {
304
        if ($this->table()->playersSatDown()->count() === 2) {
305
            return $this->table()->playersSatDown()->get(1);
306
        }
307
308 4
        return $this->table()->playersSatDown()->get($this->table()->button() + 2);
309
    }
310 4
311 1
    /**
312
     * @param PlayerContract $player
313
     */
314 3
    public function postSmallBlind(PlayerContract $player)
315
    {
316
        // Take chips from player
317
        $chips = $this->smallBlind();
318
319
        $this->postBlind($player, $chips);
320 34
321
        $this->playerActions()->push(new Action($player, Action::SMALL_BLIND, $this->smallBlind()));
322
        $this->leftToAct = $this->leftToAct()->playerHasActioned($player, LeftToAct::SMALL_BLIND);
323 34
    }
324
325 34
    /**
326
     * @param PlayerContract $player
327 34
     */
328 34
    public function postBigBlind(PlayerContract $player)
329 34
    {
330
        // Take chips from player
331
        $chips = $this->bigBlind();
332
333
        $this->postBlind($player, $chips);
334 34
335
        $this->playerActions()->push(new Action($player, Action::BIG_BLIND, $this->bigBlind()));
336
        $this->leftToAct = $this->leftToAct()->playerHasActioned($player, LeftToAct::BIG_BLIND);
337 34
    }
338
339 34
    /**
340
     * @return Chips
341 34
     */
342 34
    private function smallBlind(): Chips
343 34
    {
344
        return Chips::fromAmount($this->gameRules()->smallBlind()->amount());
345
    }
346
347
    /**
348 34
     * @return Chips
349
     */
350 34
    private function bigBlind(): Chips
351
    {
352
        return Chips::fromAmount($this->gameRules()->bigBlind()->amount());
353
    }
354
355
    /**
356 34
     * @return ChipPot
357
     */
358 34
    public function currentPot(): ChipPot
359
    {
360
        return $this->currentPot;
361
    }
362
363
    /**
364 14
     * @return ChipPotCollection
365
     */
366 14
    public function chipPots(): ChipPotCollection
367
    {
368
        return $this->chipPots;
369
    }
370
371
    /**
372 20
     * @param PlayerContract $player
373
     *
374 20
     * @return Chips
375
     */
376
    public function playerBetStack(PlayerContract $player): Chips
377
    {
378
        return $this->betStacks->findByPlayer($player);
379
    }
380
381
    /**
382 31
     * @param PlayerContract $player
383
     * @param Chips          $chips
384 31
     */
385
    private function postBlind(PlayerContract $player, $chips)
386
    {
387
        $player->chipStack()->subtract($chips);
388
389
        // Add chips to player's table stack
390
        $this->betStacks->put($player->name(), $chips);
391 34
    }
392
393 34
    /**
394
     * @return PlayerContract|false
395
     */
396 34
    public function whosTurnIsIt()
397 34
    {
398
        $nextPlayer = $this->leftToAct()->getNextPlayer();
399
        if ($nextPlayer === null) {
400
            return false;
401
        }
402
403
        return $this->players()
404 2
            ->filter(function (PlayerContract $player) use ($nextPlayer) {
405
                return $player->name() === $nextPlayer['player'];
406 2
            })
407
            ->first()
408 2
        ;
409 1
    }
410
411
    /**
412 1
     * @return ChipPotCollection
413
     */
414
    public function collectChipTotal(): ChipPotCollection
415
    {
416
        $allInActionsThisRound = $this->leftToAct()->filter(function (array $value) {
417
            return $value['action'] === LeftToAct::ALL_IN;
418 36
        });
419
420 36
        $orderedBetStacks = $this->betStacks()
421
            ->reject(function (Chips $chips, $playerName) {
422
                $foldedPlayer = $this->foldedPlayers()->findByName($playerName);
423 36
                if ($foldedPlayer) {
424 36
                    return true;
425
                }
426
427
                return false;
428
            })
429
            ->sortByChipAmount();
430 20
431
        if ($allInActionsThisRound->count() > 1 && $orderedBetStacks->unique()->count() > 1) {
432
            $orderedBetStacks->each(function (Chips $playerChips, $playerName) use ($orderedBetStacks) {
433 20
                $remainingStacks = $orderedBetStacks->filter(function (Chips $chips) {
434 20
                    return $chips->amount() !== 0;
435
                });
436 20
437 6
                $this->currentPot = ChipPot::create();
438
                $this->chipPots()->push($this->currentPot);
439 6
440 6
                $player = $this->players()->findByName($playerName);
441 5
                $allInAmount = Chips::fromAmount($orderedBetStacks->findByPlayer($player)->amount());
442
443
                $remainingStacks->each(function (Chips $chips, $playerName) use ($allInAmount, $orderedBetStacks) {
444 6
                    $player = $this->players()->findByName($playerName);
445 6
446 6
                    $stackChips = Chips::fromAmount($allInAmount->amount());
447
448
                    if (($chips->amount() - $stackChips->amount()) <= 0) {
449
                        $stackChips = Chips::fromAmount($chips->amount());
450 6
                    }
451 6
452
                    $chips->subtract($stackChips);
453 6
                    $this->currentPot->addChips($stackChips, $player);
454 6
                    $orderedBetStacks->put($playerName, Chips::fromAmount($chips->amount()));
455
                });
456 6
            });
457 6
458
            // sort the pots so we get rid of any empty ones
459
            $this->chipPots = $this->chipPots
460 6
                ->filter(function (ChipPot $chipPot) {
461
                    return $chipPot->total()->amount() !== 0;
462 6
                })
463
                ->values();
464 6
465 6
            // grab anyone that folded
466
            $this->betStacks()
467
                ->filter(function (Chips $chips, $playerName) {
468 6
                    $foldedPlayer = $this->foldedPlayers()->findByName($playerName);
469 6
                    if ($foldedPlayer && $chips->amount() > 0) {
470 6
                        return true;
471 6
                    }
472 6
473
                    return false;
474
                })
475 6
                ->each(function (Chips $chips, $playerName) use ($orderedBetStacks) {
476
                    $player = $this->players()->findByName($playerName);
477 6
478 6
                    $stackChips = Chips::fromAmount($chips->amount());
479 6
480
                    $chips->subtract($stackChips);
481
                    $this->chipPots->get(0)->addChips($stackChips, $player);
482 6
                    $orderedBetStacks->put($playerName, Chips::fromAmount($chips->amount()));
483
                });
484 6
        } else {
485 6
            $this->betStacks()->each(function (Chips $chips, $playerName) {
486 5
                $this->currentPot()->addChips($chips, $this->players()->findByName($playerName));
487
            });
488
        }
489 6
490 6
        $this->resetBetStacks();
491
492
        return $this->chipPots();
493 5
    }
494
495 5
    /**
496
     * Deal the Flop.
497 5
     */
498 5
    public function dealFlop()
499 5
    {
500 6
        if ($this->dealer()->communityCards()->count() !== 0) {
501
            throw RoundException::flopHasBeenDealt();
502
        }
503 14
        if ($player = $this->whosTurnIsIt()) {
504 14
            throw RoundException::playerStillNeedsToAct($player);
505
        }
506
507 20
        $this->collectChipTotal();
508
509 20
        $seat = $this->table()->findSeat($this->playerWithSmallBlind());
510
        $this->resetPlayerList($seat);
511
512
        $this->dealer()->dealCommunityCards(3);
513
    }
514
515 17
    /**
516
     * Deal the turn card.
517 17
     */
518 1
    public function dealTurn()
519
    {
520 17
        if ($this->dealer()->communityCards()->count() !== 3) {
521 1
            throw RoundException::turnHasBeenDealt();
522
        }
523
        if (($player = $this->whosTurnIsIt()) !== false) {
524 16
            throw RoundException::playerStillNeedsToAct($player);
525
        }
526 16
527 16
        $this->collectChipTotal();
528 16
529 16
        $seat = $this->table()->findSeat($this->playerWithSmallBlind());
530 16
        $this->resetPlayerList($seat);
531
532
        $this->dealer()->dealCommunityCards(1);
533 16
    }
534
535
    /**
536 16
     * Deal the river card.
537 16
     */
538 16
    public function dealRiver()
539 16
    {
540
        if ($this->dealer()->communityCards()->count() !== 4) {
541
            throw RoundException::riverHasBeenDealt();
542
        }
543
        if (($player = $this->whosTurnIsIt()) !== false) {
544 14
            throw RoundException::playerStillNeedsToAct($player);
545
        }
546 14
547 2
        $this->collectChipTotal();
548
549 13
        $seat = $this->table()->findSeat($this->playerWithSmallBlind());
550 1
        $this->resetPlayerList($seat);
551
552
        $this->dealer()->dealCommunityCards(1);
553 12
    }
554 12
555
    /**
556
     * @throws RoundException
557
     */
558
    public function checkPlayerTryingToAct(PlayerContract $player)
559 11
    {
560
        $actualPlayer = $this->whosTurnIsIt();
561 11
        if ($actualPlayer === false) {
562 2
            throw RoundException::noPlayerActionsNeeded();
563
        }
564 10
        if ($player !== $actualPlayer) {
565 1
            throw RoundException::playerTryingToActOutOfTurn($player, $actualPlayer);
566
        }
567
    }
568 9
569 9
    /**
570
     * @param PlayerContract $player
571
     *
572
     * @throws RoundException
573
     */
574 12
    public function playerCalls(PlayerContract $player)
575
    {
576 12
        $this->checkPlayerTryingToAct($player);
577
578 12
        $highestChipBet = $this->highestBet();
579 12
580 12
        // current highest bet - currentPlayersChipStack
581 12
        $amountLeftToBet = Chips::fromAmount($highestChipBet->amount() - $this->playerBetStack($player)->amount());
582 12
583
        $chipStackLeft = Chips::fromAmount($player->chipStack()->amount() - $amountLeftToBet->amount());
584
585 12
        $action = $chipStackLeft->amount() === 0 ? Action::ALLIN : Action::CALL;
586
        $this->playerActions->push(new Action($player, $action, $amountLeftToBet));
587
588 12
        $this->placeChipBet($player, $amountLeftToBet);
589 12
590
        $action = $chipStackLeft->amount() === 0 ? LeftToAct::ALL_IN : LeftToAct::ACTIONED;
591
        $this->leftToAct = $this->leftToAct()->playerHasActioned($player, $action);
592
    }
593
594 33
    /**
595
     * @param PlayerContract $player
596 33
     * @param Chips          $chips
597 33
     *
598 1
     * @throws RoundException
599
     */
600 33
    public function playerRaises(PlayerContract $player, Chips $chips)
601 2
    {
602
        $this->checkPlayerTryingToAct($player);
603 31
604
        $highestChipBet = $this->highestBet();
605
        if ($chips->amount() < $highestChipBet->amount()) {
606
            throw RoundException::raiseNotHighEnough($chips, $highestChipBet);
607
        }
608
609
        $chipStackLeft = Chips::fromAmount($player->chipStack()->amount() - $chips->amount());
610 22
611
        $action = $chipStackLeft->amount() === 0 ? Action::ALLIN : Action::RAISE;
612 22
        $this->playerActions->push(new Action($player, $action, $chips));
613
614 22
        $this->placeChipBet($player, $chips);
615
616
        $action = $chipStackLeft->amount() === 0 ? LeftToAct::ALL_IN : LeftToAct::AGGRESSIVELY_ACTIONED;
617 22
        $this->leftToAct = $this->leftToAct()->playerHasActioned($player, $action);
618
    }
619 22
620
    /**
621 22
     * @param PlayerContract $player
622 22
     *
623 22
     * @throws RoundException
624
     */
625
    public function playerFoldsHand(PlayerContract $player)
626
    {
627
        $this->checkPlayerTryingToAct($player);
628
629
        $this->playerActions()->push(new Action($player, Action::FOLD));
630
631 4
        $this->foldedPlayers->push($player);
632
        $this->leftToAct = $this->leftToAct()->removePlayer($player);
633 4
    }
634
635 4
    /**
636
     * @param PlayerContract $player
637 4
     *
638 3
     * @throws RoundException
639 3
     */
640
    public function playerPushesAllIn(PlayerContract $player)
641
    {
642
        $this->checkPlayerTryingToAct($player);
643
644
        // got the players chipStack
645
        $chips = $player->chipStack();
646 14
647
        // gotta create a new chip obj here cause of PHPs /awesome/ objRef ability :D
648 14
        $this->playerActions()->push(new Action($player, Action::ALLIN, Chips::fromAmount($chips->amount())));
649
650 13
        $this->placeChipBet($player, $chips);
651
        $this->leftToAct = $this->leftToAct()->playerHasActioned($player, LeftToAct::ALL_IN);
652 13
    }
653 13
654 13
    /**
655
     * @param PlayerContract $player
656
     *
657
     * @throws RoundException
658
     */
659
    public function playerChecks(PlayerContract $player)
660
    {
661 14
        $this->checkPlayerTryingToAct($player);
662
663 14
        if ($this->playerBetStack($player)->amount() !== $this->betStacks()->max()->amount()) {
664
            throw RoundException::cantCheckWithBetActive();
665
        }
666 14
667
        $this->playerActions()->push(new Action($player, Action::CHECK));
668
        $this->leftToAct = $this->leftToAct()->playerHasActioned($player, LeftToAct::ACTIONED);
669 14
    }
670
671 14
    /**
672 14
     * @return Chips
673 14
     */
674
    private function highestBet(): Chips
675
    {
676
        return Chips::fromAmount($this->betStacks()->max(function (Chips $chips) {
677
            return $chips->amount();
678
        }) ?? 0);
679
    }
680 16
681
    /**
682 16
     * @param PlayerContract $player
683
     * @param Chips          $chips
684 15
     */
685 15
    private function placeChipBet(PlayerContract $player, Chips $chips)
686 15
    {
687
        if ($player->chipStack()->amount() < $chips->amount()) {
688
            throw RoundException::notEnoughChipsInChipStack($player, $chips);
689
        }
690
691 22
        // add the chips to the players tableStack first
692
        $this->playerBetStack($player)->add($chips);
693
694 22
        // then remove it off their actual stack
695 22
        $player->bet($chips);
696
    }
697
698
    /**
699
     * Reset the chip stack for all players.
700
     */
701
    private function resetBetStacks()
702 30
    {
703
        $this->players()->each(function (PlayerContract $player) {
704 30
            $this->betStacks->put($player->name(), Chips::zero());
705 1
        });
706
    }
707
708
    /**
709 30
     * Reset the leftToAct collection.
710
     */
711
    private function setupLeftToAct()
712 30
    {
713 30
        if ($this->players()->count() === 2) {
714
            $this->leftToAct = $this->leftToAct()->setup($this->players());
715
716
            return;
717
        }
718
719
        $this->leftToAct = $this->leftToAct
720 50
            ->setup($this->players())
721 50
            ->resetPlayerListFromSeat($this->table()->button() + 1);
722 50
    }
723 50
724
    /**
725
     * @param PlayerContract $player
726
     */
727
    public function sitPlayerOut(PlayerContract $player)
728 50
    {
729
        $this->table()->sitPlayerOut($player);
730 50
        $this->leftToAct = $this->leftToAct()->removePlayer($player);
731 4
    }
732
733 4
    /**
734
     * @var int
735
     */
736 46
    public function resetPlayerList(int $seat)
737 46
    {
738 46
        $this->leftToAct = $this->leftToAct
739 46
            ->resetActions()
740
            ->sortBySeats()
741
            ->resetPlayerListFromSeat($seat);
742
    }
743
}
744