/* The MIT License (MIT) Copyright (c) 2013 Jacob Kanipe-Illig Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.hyphenated.card.service; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.hyphenated.card.Card; import com.hyphenated.card.Deck; import com.hyphenated.card.dao.GameDao; import com.hyphenated.card.dao.HandDao; import com.hyphenated.card.dao.PlayerDao; import com.hyphenated.card.domain.BlindLevel; import com.hyphenated.card.domain.BoardEntity; import com.hyphenated.card.domain.Game; import com.hyphenated.card.domain.HandEntity; import com.hyphenated.card.domain.Player; import com.hyphenated.card.domain.PlayerHand; import com.hyphenated.card.util.PlayerHandBetAmountComparator; import com.hyphenated.card.util.PlayerUtil; import com.hyphenated.card.view.GameAction; @Service public class PokerHandServiceImpl implements PokerHandService { @Autowired private HandDao handDao; @Autowired private GameDao gameDao; @Autowired private PlayerDao playerDao; @Override @Transactional @GameAction public HandEntity startNewHand(Game game) { game = gameDao.merge(game); HandEntity hand = new HandEntity(); updateBlindLevel(game); hand.setBlindLevel(game.getGameStructure().getCurrentBlindLevel()); hand.setGame(game); Deck d = new Deck(true); Set<PlayerHand> participatingPlayers = new HashSet<PlayerHand>(); for(Player p : game.getPlayers()){ if(p.getChips() > 0){ PlayerHand ph = new PlayerHand(); ph.setHandEntity(hand); ph.setPlayer(p); ph.setCard1(d.dealCard()); ph.setCard2(d.dealCard()); participatingPlayers.add(ph); } } hand.setPlayers(participatingPlayers); //Sort and get the next player to act (immediately after the big blind) List<PlayerHand> players = new ArrayList<PlayerHand>(); players.addAll(participatingPlayers); Player nextToAct = PlayerUtil.getNextPlayerToAct(hand, this.getPlayerInBB(hand)); hand.setCurrentToAct(nextToAct); //Register the Forced Small and Big Blind bets as part of the hand Player smallBlind = getPlayerInSB(hand); Player bigBlind = getPlayerInBB(hand); int sbBet = 0; int bbBet = 0; for (PlayerHand ph : hand.getPlayers()){ if(ph.getPlayer().equals(smallBlind)){ sbBet = Math.min(hand.getBlindLevel().getSmallBlind(), smallBlind.getChips()); ph.setBetAmount(sbBet); ph.setRoundBetAmount(sbBet); smallBlind.setChips(smallBlind.getChips() - sbBet); } else if(ph.getPlayer().equals(bigBlind)){ bbBet = Math.min(hand.getBlindLevel().getBigBlind(), bigBlind.getChips()); ph.setBetAmount(bbBet); ph.setRoundBetAmount(bbBet); bigBlind.setChips(bigBlind.getChips() - bbBet); } } hand.setTotalBetAmount(hand.getBlindLevel().getBigBlind()); hand.setLastBetAmount(hand.getBlindLevel().getBigBlind()); hand.setPot(sbBet + bbBet); BoardEntity b = new BoardEntity(); hand.setBoard(b); hand.setCards(d.exportDeck()); hand = handDao.save(hand); game.setCurrentHand(hand); gameDao.save(game); return hand; } @Override @Transactional @GameAction public void endHand(HandEntity hand){ hand = handDao.merge(hand); if(!isActionResolved(hand)){ throw new IllegalStateException("There are unresolved betting actions"); } Game game = hand.getGame(); hand.setCurrentToAct(null); determineWinner(hand); //If players were eliminated this hand, set their finished position List<PlayerHand> phs = new ArrayList<PlayerHand>(); phs.addAll(hand.getPlayers()); //Sort the list of PlayerHands so the one with the smallest chips at risk is first. //Use this to determine finish position if multiple players are eliminated on the same hand. Collections.sort(phs, new PlayerHandBetAmountComparator()); for(PlayerHand ph : phs){ if(ph.getPlayer().getChips() <= 0){ ph.getPlayer().setFinishPosition(game.getPlayersRemaining()); game.setPlayersRemaining(game.getPlayersRemaining() - 1); playerDao.save(ph.getPlayer()); } } //For all players in the game, remove any who are out of chips (eliminated) List<Player> players = new ArrayList<Player>(); for(Player p : game.getPlayers()){ if(p.getChips() != 0){ players.add(p); } else if(p.equals(game.getPlayerInBTN())){ //If the player on the Button has been eliminated, we still need this player //in the list so that we calculate next button from its position players.add(p); } } if(game.getPlayersRemaining() < 2){ //TODO end game. Move to start hand? } //Rotate Button. Use Simplified Moving Button algorithm (for ease of coding) //This means we always rotate button. Blinds will be next two active players. May skip blinds. Player nextButton = PlayerUtil.getNextPlayerInGameOrder(players, game.getPlayerInBTN()); game.setPlayerInBTN(nextButton); gameDao.merge(game); //Remove Deck from database. No need to keep that around anymore hand.setCards(new ArrayList<Card>()); handDao.merge(hand); } @Override @Transactional(readOnly=true) public HandEntity getHandById(long id) { return handDao.findById(id); } @Override @Transactional public HandEntity saveHand(HandEntity hand) { return handDao.save(hand); } @Override @Transactional @GameAction public HandEntity flop(HandEntity hand) throws IllegalStateException { if(hand.getBoard().getFlop1() != null){ throw new IllegalStateException("Unexpected Flop."); } //Re-attach to persistent context for this transaction (Lazy Loading stuff) hand = handDao.merge(hand); if(!isActionResolved(hand)){ throw new IllegalStateException("There are unresolved preflop actions"); } Deck d = new Deck(hand.getCards()); d.shuffleDeck(); BoardEntity board = hand.getBoard(); board.setFlop1(d.dealCard()); board.setFlop2(d.dealCard()); board.setFlop3(d.dealCard()); hand.setCards(d.exportDeck()); resetRoundValues(hand); return handDao.merge(hand); } @Override @Transactional @GameAction public HandEntity turn(HandEntity hand) throws IllegalStateException{ if(hand.getBoard().getFlop1() == null || hand.getBoard().getTurn()!= null){ throw new IllegalStateException("Unexpected Turn."); } //Re-attach to persistent context for this transaction (Lazy Loading stuff) hand = handDao.merge(hand); if(!isActionResolved(hand)){ throw new IllegalStateException("There are unresolved flop actions"); } Deck d = new Deck(hand.getCards()); d.shuffleDeck(); BoardEntity board = hand.getBoard(); board.setTurn(d.dealCard()); hand.setCards(d.exportDeck()); resetRoundValues(hand); return handDao.merge(hand); } @Override @Transactional @GameAction public HandEntity river(HandEntity hand) throws IllegalStateException{ if(hand.getBoard().getFlop1() == null || hand.getBoard().getTurn() == null || hand.getBoard().getRiver() != null){ throw new IllegalStateException("Unexpected River."); } //Re-attach to persistent context for this transaction (Lazy Loading stuff) hand = handDao.merge(hand); if(!isActionResolved(hand)){ throw new IllegalStateException("There are unresolved turn actions"); } Deck d = new Deck(hand.getCards()); d.shuffleDeck(); BoardEntity board = hand.getBoard(); board.setRiver(d.dealCard()); hand.setCards(d.exportDeck()); resetRoundValues(hand); return handDao.merge(hand); } @Override @Transactional @GameAction public boolean sitOutCurrentPlayer(HandEntity hand){ hand = handDao.merge(hand); Player currentPlayer = hand.getCurrentToAct(); if(currentPlayer == null){ return false; } currentPlayer.setSittingOut(true); playerDao.save(currentPlayer); PlayerHand playerHand = null; for(PlayerHand ph : hand.getPlayers()){ if(ph.getPlayer().equals(currentPlayer)){ playerHand = ph; break; } } //Move action to the next player Player next = PlayerUtil.getNextPlayerToAct(hand, currentPlayer); //If the player being sat out needs to call, that player is folded if(playerHand != null && hand.getTotalBetAmount() > playerHand.getRoundBetAmount()){ PlayerUtil.removePlayerFromHand(currentPlayer, hand); } hand.setCurrentToAct(next); handDao.save(hand); return true; } @Override public Player getPlayerInSB(HandEntity hand){ Player button = hand.getGame().getPlayerInBTN(); //Heads up the Button is the Small Blind if(hand.getPlayers().size() == 2){ return button; } List<PlayerHand> players = new ArrayList<PlayerHand>(); players.addAll(hand.getPlayers()); return PlayerUtil.getNextPlayerInGameOrderPH(players, button); } @Override public Player getPlayerInBB(HandEntity hand){ Player button = hand.getGame().getPlayerInBTN(); List<PlayerHand> players = new ArrayList<PlayerHand>(); players.addAll(hand.getPlayers()); Player leftOfButton = PlayerUtil.getNextPlayerInGameOrderPH(players, button); //Heads up, the player who is not the Button is the Big blind if(hand.getPlayers().size() == 2){ return leftOfButton; } return PlayerUtil.getNextPlayerInGameOrderPH(players, leftOfButton); } private void updateBlindLevel(Game game){ if(game.getGameStructure().getCurrentBlindEndTime() == null){ //Start the blind setNewBlindEndTime(game); } else if(game.getGameStructure().getCurrentBlindEndTime().before(new Date())){ //Time has expired, next blind level List<BlindLevel> blinds = game.getGameStructure().getBlindLevels(); Collections.sort(blinds); boolean nextBlind = false; for(BlindLevel blind : blinds){ if(nextBlind){ game.getGameStructure().setCurrentBlindLevel(blind); setNewBlindEndTime(game); break; } if(blind == game.getGameStructure().getCurrentBlindLevel()){ nextBlind = true; } } } } private void setNewBlindEndTime(Game game){ Calendar c = Calendar.getInstance(); c.add(Calendar.MINUTE, game.getGameStructure().getBlindLength()); game.getGameStructure().setCurrentBlindEndTime(c.getTime()); } private void resetRoundValues(HandEntity hand){ hand.setTotalBetAmount(0); hand.setLastBetAmount(0); List<Player> playersInHand = new ArrayList<Player>(); for(PlayerHand ph : hand.getPlayers()){ ph.setRoundBetAmount(0); playersInHand.add(ph.getPlayer()); } //Next player is to the left of the button. Given that the button may have been eliminated //In a round of betting, we need to put the button back to determine relative position. Player btn = hand.getGame().getPlayerInBTN(); if(!playersInHand.contains(btn)){ playersInHand.add(btn); } Player next = PlayerUtil.getNextPlayerInGameOrder(playersInHand, btn); Player firstNext = next; //Skip all in players and players that are sitting out while(next.getChips() <= 0 || next.isSittingOut()){ next = PlayerUtil.getNextPlayerInGameOrder(playersInHand, next); if(next.equals(firstNext)){ //Exit condition if all players are all in. break; } } hand.setCurrentToAct(next); } private void determineWinner(HandEntity hand){ //if only one PH left, everyone else folded if(hand.getPlayers().size() == 1){ Player winner = hand.getPlayers().iterator().next().getPlayer(); winner.setChips(winner.getChips() + hand.getPot()); playerDao.merge(winner); } else{ //Refund all in overbet player if applicable before determining winner refundOverbet(hand); //Iterate through map of players to there amount won. Persist. Map<Player, Integer> winners = PlayerUtil.getAmountWonInHandForAllPlayers(hand); if(winners == null){ return; } for(Map.Entry<Player, Integer> entry : winners.entrySet()){ Player player = entry.getKey(); player.setChips(player.getChips() + entry.getValue()); playerDao.merge(player); } } } private void refundOverbet(HandEntity hand){ List<PlayerHand> phs = new ArrayList<PlayerHand>(); phs.addAll(hand.getPlayers()); //Sort from most money contributed, to the least Collections.sort(phs, new PlayerHandBetAmountComparator()); Collections.reverse(phs); //If there are at least 2 players, and the top player contributed more to the pot than the next player if(phs.size() >= 2 && (phs.get(0).getBetAmount() > phs.get(1).getBetAmount())){ //Refund that extra amount contributed. Remove from pot, add back to player int diff = phs.get(0).getBetAmount() - phs.get(1).getBetAmount(); phs.get(0).setBetAmount(phs.get(1).getBetAmount()); phs.get(0).getPlayer().setChips(phs.get(0).getPlayer().getChips() + diff); hand.setPot(hand.getPot() - diff); } } //Helper method to see if there are any outstanding actions left in a betting round private boolean isActionResolved(HandEntity hand){ int roundBetAmount = hand.getTotalBetAmount(); for(PlayerHand ph : hand.getPlayers()){ //All players should have paid the roundBetAmount or should be all in if(ph.getRoundBetAmount() != roundBetAmount && ph.getPlayer().getChips() > 0){ return false; } } return true; } }