src/Game/Round.php
<?php
namespace Cysha\Casino\Holdem\Game;
use Cysha\Casino\Cards\Contracts\CardResults;
use Cysha\Casino\Game\Chips;
use Cysha\Casino\Game\ChipStackCollection;
use Cysha\Casino\Game\Contracts\Dealer as DealerContract;
use Cysha\Casino\Game\Contracts\GameParameters;
use Cysha\Casino\Game\Contracts\Player as PlayerContract;
use Cysha\Casino\Game\PlayerCollection;
use Cysha\Casino\Holdem\Exceptions\RoundException;
use Cysha\Casino\Holdem\Game\LeftToAct;
use Ramsey\Uuid\Uuid;
class Round
{
/**
* @var Uuid
*/
private $id;
/**
* @var Table
*/
private $table;
/**
* @var ChipStackCollection
*/
private $betStacks;
/**
* @var PlayerCollection
*/
private $foldedPlayers;
/**
* @var ChipPotCollection
*/
private $chipPots;
/**
* @var ChipPot
*/
private $currentPot;
/**
* @var ActionCollection
*/
private $actions;
/**
* @var PlayerCollection
*/
private $leftToAct;
/**
* @var GameParameters
*/
private $gameRules;
/**
* Round constructor.
*
* @param Uuid $id
* @param Table $table
* @param GameParameters $gameRules
*/
private function __construct(Uuid $id, Table $table, GameParameters $gameRules)
{
$this->id = $id;
$this->table = $table;
$this->chipPots = ChipPotCollection::make();
$this->currentPot = ChipPot::create();
$this->betStacks = ChipStackCollection::make();
$this->foldedPlayers = PlayerCollection::make();
$this->actions = ActionCollection::make();
$this->leftToAct = LeftToAct::make();
$this->gameRules = $gameRules;
// shuffle the deck ready
$this->dealer()->shuffleDeck();
// add the default pot to the chipPots
$this->chipPots->push($this->currentPot);
// init the betStacks and actions for each player
$this->resetBetStacks();
$this->setupLeftToAct();
}
/**
* Start a Round of poker.
*
* @param Uuid $id
* @param Table $table
* @param GameParameters $gameRules
*
* @return Round
*/
public static function start(Uuid $id, Table $table, GameParameters $gameRules): Round
{
return new static($id, $table, $gameRules);
}
/**
* Run the cleanup procedure for an end of Round.
*/
public function end()
{
$this->dealer()->checkCommunityCards();
$this->collectChipTotal();
$this->distributeWinnings();
$this->table()->moveButton();
}
/**
* @return Uuid
*/
public function id(): Uuid
{
return $this->id;
}
/**
* @return DealerContract
*/
public function dealer(): DealerContract
{
return $this->table->dealer();
}
/**
* @return PlayerCollection
*/
public function players(): PlayerCollection
{
return $this->table->players();
}
/**
* @return PlayerCollection
*/
public function playersStillIn(): PlayerCollection
{
return $this->table->playersSatDown()->diff($this->foldedPlayers());
}
/**
* @return PlayerCollection
*/
public function foldedPlayers(): PlayerCollection
{
return $this->foldedPlayers;
}
/**
* @return ActionCollection
*/
public function actions(): ActionCollection
{
return $this->actions;
}
/**
* @return LeftToAct
*/
public function leftToAct(): LeftToAct
{
return $this->leftToAct;
}
/**
* @return Table
*/
public function table(): Table
{
return $this->table;
}
/**
* @return ChipStackCollection
*/
public function betStacks(): ChipStackCollection
{
return $this->betStacks;
}
/**
* @return GameParameters
*/
public function gameRules(): GameParameters
{
return $this->gameRules;
}
/**
* @return int
*/
public function betStacksTotal(): int
{
return $this->betStacks()->total()->amount();
}
public function dealHands()
{
$players = $this->table()
->playersSatDown()
->resetPlayerListFromSeat($this->table()->button() + 1);
$this->dealer()->dealHands($players);
}
/**
* Runs over each chipPot and assigns the chips to the winning player.
*/
private function distributeWinnings()
{
$this->chipPots()
->reverse()
->each(function (ChipPot $chipPot) {
// if only 1 player participated to pot, he wins it no arguments
if ($chipPot->players()->count() === 1) {
$potTotal = $chipPot->chips()->total();
$chipPot->players()->first()->chipStack()->add($potTotal);
$this->chipPots()->remove($chipPot);
return;
}
$activePlayers = $chipPot->players()->diff($this->foldedPlayers());
$playerHands = $this->dealer()->hands()->findByPlayers($activePlayers);
$evaluate = $this->dealer()->evaluateHands($this->dealer()->communityCards(), $playerHands);
// if just 1, the player with that hand wins
if ($evaluate->count() === 1) {
$player = $evaluate->first()->hand()->player();
$potTotal = $chipPot->chips()->total();
$player->chipStack()->add($potTotal);
$this->chipPots()->remove($chipPot);
} else {
// if > 1 hand is evaluated as highest, split the pot evenly between the players
$potTotal = $chipPot->chips()->total();
// split the pot between the number of players
$splitTotal = Chips::fromAmount(($potTotal->amount() / $evaluate->count()));
$evaluate->each(function (CardResults $result) use ($splitTotal) {
$result->hand()->player()->chipStack()->add($splitTotal);
});
$this->chipPots()->remove($chipPot);
}
})
;
}
/**
* @param Player $actualPlayer
*
* @return bool
*/
public function playerIsStillIn(PlayerContract $actualPlayer)
{
$playerCount = $this->playersStillIn()->filter->equals($actualPlayer)->count();
return $playerCount === 1;
}
/**
* @return PlayerContract
*/
public function playerWithButton(): PlayerContract
{
return $this->table()->locatePlayerWithButton();
}
/**
* @return PlayerContract
*/
public function playerWithSmallBlind(): PlayerContract
{
if ($this->table()->playersSatDown()->count() === 2) {
return $this->table()->playersSatDown()->get($this->table()->button());
}
return $this->table()->playersSatDown()->get($this->table()->button() + 1);
}
/**
* @return PlayerContract
*/
public function playerWithBigBlind(): PlayerContract
{
if ($this->table()->playersSatDown()->count() === 2) {
$idx = $this->table()->button();
$idx = ($idx === 1) ? $idx - 1 : $idx + 1;
return $this->table()->playersSatDown()->get($idx);
}
return $this->table()->playersSatDown()->get($this->table()->button() + 2);
}
/**
* @param PlayerContract $player
*/
public function postSmallBlind(PlayerContract $player)
{
// Take chips from player
$chips = $this->smallBlind();
$this->postBlind($player, $chips);
$this->actions()->push(new Action($player, Action::SMALL_BLIND, ['chips' => $this->smallBlind()]));
$this->leftToAct = $this->leftToAct()->playerHasActioned($player, LeftToAct::SMALL_BLIND);
}
/**
* @param PlayerContract $player
*/
public function postBigBlind(PlayerContract $player)
{
// Take chips from player
$chips = $this->bigBlind();
$this->postBlind($player, $chips);
$this->actions()->push(new Action($player, Action::BIG_BLIND, ['chips' => $this->bigBlind()]));
$this->leftToAct = $this->leftToAct()->playerHasActioned($player, LeftToAct::BIG_BLIND);
}
/**
* @return Chips
*/
private function smallBlind(): Chips
{
return Chips::fromAmount($this->gameRules()->smallBlind()->amount());
}
/**
* @return Chips
*/
private function bigBlind(): Chips
{
return Chips::fromAmount($this->gameRules()->bigBlind()->amount());
}
/**
* @return ChipPot
*/
public function currentPot(): ChipPot
{
return $this->currentPot;
}
/**
* @return ChipPotCollection
*/
public function chipPots(): ChipPotCollection
{
return $this->chipPots;
}
/**
* @param PlayerContract $player
*
* @return Chips
*/
public function playerBetStack(PlayerContract $player): Chips
{
return $this->betStacks->findByPlayer($player);
}
/**
* @param PlayerContract $player
* @param Chips $chips
*/
private function postBlind(PlayerContract $player, $chips)
{
$player->chipStack()->subtract($chips);
// Add chips to player's table stack
$this->betStacks->put($player->name(), $chips);
}
/**
* @return PlayerContract|false
*/
public function whosTurnIsIt()
{
$nextPlayer = $this->leftToAct()->getNextPlayer();
if ($nextPlayer === null) {
return false;
}
return $this->players()
->filter(function (PlayerContract $player) use ($nextPlayer) {
return $player->name() === $nextPlayer['player'];
})
->first()
;
}
/**
* @return ChipPotCollection
*/
public function collectChipTotal(): ChipPotCollection
{
$allInActionsThisRound = $this->leftToAct()->filter(function (array $value) {
return $value['action'] === LeftToAct::ALLIN;
});
$orderedBetStacks = $this->betStacks()
->reject(function (Chips $chips, $playerName) {
$foldedPlayer = $this->foldedPlayers()->findByName($playerName);
if ($foldedPlayer) {
return true;
}
return false;
})
->sortByChipAmount();
if ($allInActionsThisRound->count() > 1 && $orderedBetStacks->unique()->count() > 1) {
$orderedBetStacks->each(function (Chips $playerChips, $playerName) use ($orderedBetStacks) {
$remainingStacks = $orderedBetStacks->filter(function (Chips $chips) {
return $chips->amount() !== 0;
});
$this->currentPot = ChipPot::create();
$this->chipPots()->push($this->currentPot);
$player = $this->players()->findByName($playerName);
$allInAmount = Chips::fromAmount($orderedBetStacks->findByPlayer($player)->amount());
$remainingStacks->each(function (Chips $chips, $playerName) use ($allInAmount, $orderedBetStacks) {
$player = $this->players()->findByName($playerName);
$stackChips = Chips::fromAmount($allInAmount->amount());
if (($chips->amount() - $stackChips->amount()) <= 0) {
$stackChips = Chips::fromAmount($chips->amount());
}
$chips->subtract($stackChips);
$this->currentPot->addChips($stackChips, $player);
$orderedBetStacks->put($playerName, Chips::fromAmount($chips->amount()));
});
});
// sort the pots so we get rid of any empty ones
$this->chipPots = $this->chipPots
->filter(function (ChipPot $chipPot) {
return $chipPot->total()->amount() !== 0;
})
->values();
// grab anyone that folded
$this->betStacks()
->filter(function (Chips $chips, $playerName) {
$foldedPlayer = $this->foldedPlayers()->findByName($playerName);
if ($foldedPlayer && $chips->amount() > 0) {
return true;
}
return false;
})
->each(function (Chips $chips, $playerName) use ($orderedBetStacks) {
$player = $this->players()->findByName($playerName);
$stackChips = Chips::fromAmount($chips->amount());
$chips->subtract($stackChips);
$this->chipPots->get(0)->addChips($stackChips, $player);
$orderedBetStacks->put($playerName, Chips::fromAmount($chips->amount()));
});
} else {
$this->betStacks()->each(function (Chips $chips, $playerName) {
$this->currentPot()->addChips($chips, $this->players()->findByName($playerName));
});
}
$this->resetBetStacks();
return $this->chipPots();
}
/**
* Deal the Flop.
*/
public function dealFlop()
{
if ($this->dealer()->communityCards()->count() !== 0) {
throw RoundException::flopHasBeenDealt();
}
if ($player = $this->whosTurnIsIt()) {
throw RoundException::playerStillNeedsToAct($player);
}
$this->collectChipTotal();
$seat = $this->table()->findSeat($this->playerWithSmallBlind());
$this->resetPlayerList($seat);
$this->dealer()->dealCommunityCards(3);
$this->actions()->push(new Action($this->dealer(), Action::DEALT_FLOP, [
'communityCards' => $this->dealer()->communityCards()->only(range(0, 2)),
]));
}
/**
* Deal the turn card.
*/
public function dealTurn()
{
if ($this->dealer()->communityCards()->count() !== 3) {
throw RoundException::turnHasBeenDealt();
}
if (($player = $this->whosTurnIsIt()) !== false) {
throw RoundException::playerStillNeedsToAct($player);
}
$this->collectChipTotal();
$seat = $this->table()->findSeat($this->playerWithSmallBlind());
$this->resetPlayerList($seat);
$this->dealer()->dealCommunityCards(1);
$this->actions()->push(new Action($this->dealer(), Action::DEALT_TURN, [
'communityCards' => $this->dealer()->communityCards()->only(3),
]));
}
/**
* Deal the river card.
*/
public function dealRiver()
{
if ($this->dealer()->communityCards()->count() !== 4) {
throw RoundException::riverHasBeenDealt();
}
if (($player = $this->whosTurnIsIt()) !== false) {
throw RoundException::playerStillNeedsToAct($player);
}
$this->collectChipTotal();
$seat = $this->table()->findSeat($this->playerWithSmallBlind());
$this->resetPlayerList($seat);
$this->dealer()->dealCommunityCards(1);
$this->actions()->push(new Action($this->dealer(), Action::DEALT_RIVER, [
'communityCards' => $this->dealer()->communityCards()->only(4),
]));
}
/**
* @throws RoundException
*/
public function checkPlayerTryingToAct(PlayerContract $player)
{
$actualPlayer = $this->whosTurnIsIt();
if ($actualPlayer === false) {
throw RoundException::noPlayerActionsNeeded();
}
if ($player !== $actualPlayer) {
throw RoundException::playerTryingToActOutOfTurn($player, $actualPlayer);
}
}
/**
* @param PlayerContract $player
*
* @throws RoundException
*/
public function playerCalls(PlayerContract $player)
{
$this->checkPlayerTryingToAct($player);
$highestChipBet = $this->highestBet();
// current highest bet - currentPlayersChipStack
$amountLeftToBet = Chips::fromAmount($highestChipBet->amount() - $this->playerBetStack($player)->amount());
$chipStackLeft = Chips::fromAmount($player->chipStack()->amount() - $amountLeftToBet->amount());
if ($chipStackLeft->amount() <= 0) {
$amountLeftToBet = Chips::fromAmount($player->chipStack()->amount());
$chipStackLeft = Chips::zero();
}
$action = $chipStackLeft->amount() === 0 ? Action::ALLIN : Action::CALL;
$this->actions->push(new Action($player, $action, ['chips' => $amountLeftToBet]));
$this->placeChipBet($player, $amountLeftToBet);
$action = $chipStackLeft->amount() === 0 ? LeftToAct::ALLIN : LeftToAct::ACTIONED;
$this->leftToAct = $this->leftToAct()->playerHasActioned($player, $action);
}
/**
* @param PlayerContract $player
* @param Chips $chips
*
* @throws RoundException
*/
public function playerRaises(PlayerContract $player, Chips $chips)
{
$this->checkPlayerTryingToAct($player);
$highestChipBet = $this->highestBet();
if ($chips->amount() < $highestChipBet->amount()) {
throw RoundException::raiseNotHighEnough($chips, $highestChipBet);
}
$chipStackLeft = Chips::fromAmount($player->chipStack()->amount() - $chips->amount());
if ($chipStackLeft->amount() === 0) {
return $this->playerPushesAllIn($player, $chips);
}
$betAmount = Chips::fromAmount($highestChipBet->amount() + $chips->amount());
$this->actions->push(new Action($player, Action::RAISE, ['chips' => $betAmount]));
$this->placeChipBet($player, $betAmount);
$action = $chipStackLeft->amount() === 0 ? LeftToAct::ALLIN : LeftToAct::AGGRESSIVELY_ACTIONED;
$this->leftToAct = $this->leftToAct()->playerHasActioned($player, $action);
}
/**
* @param PlayerContract $player
*
* @throws RoundException
*/
public function playerFoldsHand(PlayerContract $player)
{
$this->checkPlayerTryingToAct($player);
$this->actions()->push(new Action($player, Action::FOLD));
$this->foldedPlayers->push($player);
$this->leftToAct = $this->leftToAct()->removePlayer($player);
}
/**
* @param PlayerContract $player
*
* @throws RoundException
*/
public function playerPushesAllIn(PlayerContract $player)
{
$this->checkPlayerTryingToAct($player);
// got the players chipStack
$chips = $player->chipStack();
// gotta create a new chip obj here cause of PHPs /awesome/ objRef ability :D
$this->actions()->push(new Action($player, Action::ALLIN, ['chips' => Chips::fromAmount($chips->amount())]));
$this->placeChipBet($player, $chips);
$this->leftToAct = $this->leftToAct()->playerHasActioned($player, LeftToAct::ALLIN);
}
/**
* @param PlayerContract $player
*
* @throws RoundException
*/
public function playerChecks(PlayerContract $player)
{
$this->checkPlayerTryingToAct($player);
if ($this->playerBetStack($player)->amount() !== $this->betStacks()->max()->amount()) {
throw RoundException::cantCheckWithBetActive();
}
$this->actions()->push(new Action($player, Action::CHECK));
$this->leftToAct = $this->leftToAct()->playerHasActioned($player, LeftToAct::ACTIONED);
}
/**
* @return Chips
*/
private function highestBet(): Chips
{
return Chips::fromAmount($this->betStacks()->max(function (Chips $chips) {
return $chips->amount();
}) ?? 0);
}
/**
* @param PlayerContract $player
* @param Chips $chips
*/
private function placeChipBet(PlayerContract $player, Chips $chips)
{
if ($player->chipStack()->amount() < $chips->amount()) {
throw RoundException::notEnoughChipsInChipStack($player, $chips);
}
// add the chips to the players tableStack first
$this->playerBetStack($player)->add($chips);
// then remove it off their actual stack
$player->bet($chips);
}
/**
* Reset the chip stack for all players.
*/
private function resetBetStacks()
{
$this->players()->each(function (PlayerContract $player) {
$this->betStacks->put($player->name(), Chips::zero());
});
}
/**
* Reset the leftToAct collection.
*/
private function setupLeftToAct()
{
if ($this->players()->count() === 2) {
$this->leftToAct = $this->leftToAct()->setup($this->players());
return;
}
$this->leftToAct = $this->leftToAct
->setup($this->players())
->resetPlayerListFromSeat($this->table()->button() + 1);
}
/**
* @param PlayerContract $player
*/
public function sitPlayerOut(PlayerContract $player)
{
$this->table()->sitPlayerOut($player);
$this->leftToAct = $this->leftToAct()->removePlayer($player);
}
/**
* @var int
*/
public function resetPlayerList(int $seat)
{
$this->leftToAct = $this->leftToAct
->resetActions()
->sortBySeats()
->resetPlayerListFromSeat($seat);
}
}