package com.flexpoker.table.command.aggregate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import com.flexpoker.exception.FlexPokerException;
import com.flexpoker.framework.command.EventApplier;
import com.flexpoker.framework.domain.AggregateRoot;
import com.flexpoker.model.PlayerAction;
import com.flexpoker.model.card.Card;
import com.flexpoker.model.card.CardsUsedInHand;
import com.flexpoker.model.card.PocketCards;
import com.flexpoker.table.command.events.ActionOnChangedEvent;
import com.flexpoker.table.command.events.AutoMoveHandForwardEvent;
import com.flexpoker.table.command.events.CardsShuffledEvent;
import com.flexpoker.table.command.events.FlopCardsDealtEvent;
import com.flexpoker.table.command.events.HandCompletedEvent;
import com.flexpoker.table.command.events.HandDealtEvent;
import com.flexpoker.table.command.events.LastToActChangedEvent;
import com.flexpoker.table.command.events.PlayerAddedEvent;
import com.flexpoker.table.command.events.PlayerBustedTableEvent;
import com.flexpoker.table.command.events.PlayerCalledEvent;
import com.flexpoker.table.command.events.PlayerCheckedEvent;
import com.flexpoker.table.command.events.PlayerFoldedEvent;
import com.flexpoker.table.command.events.PlayerForceCheckedEvent;
import com.flexpoker.table.command.events.PlayerForceFoldedEvent;
import com.flexpoker.table.command.events.PlayerRaisedEvent;
import com.flexpoker.table.command.events.PlayerRemovedEvent;
import com.flexpoker.table.command.events.PotAmountIncreasedEvent;
import com.flexpoker.table.command.events.PotClosedEvent;
import com.flexpoker.table.command.events.PotCreatedEvent;
import com.flexpoker.table.command.events.RiverCardDealtEvent;
import com.flexpoker.table.command.events.RoundCompletedEvent;
import com.flexpoker.table.command.events.TableCreatedEvent;
import com.flexpoker.table.command.events.TablePausedEvent;
import com.flexpoker.table.command.events.TableResumedEvent;
import com.flexpoker.table.command.events.TurnCardDealtEvent;
import com.flexpoker.table.command.events.WinnersDeterminedEvent;
import com.flexpoker.table.command.framework.TableEvent;
public class Table extends AggregateRoot<TableEvent> {
private final UUID aggregateId;
private int aggregateVersion;
private final Map<Class<? extends TableEvent>, EventApplier<? super TableEvent>> methodTable;
private final UUID gameId;
private final Map<Integer, UUID> seatMap;
private final Map<UUID, Integer> chipsInBack;
private int buttonOnPosition;
private int smallBlindPosition;
private int bigBlindPosition;
private Hand currentHand;
private boolean paused;
protected Table(boolean creatingFromEvents, UUID aggregateId, UUID gameId,
Map<Integer, UUID> seatMap, int startingNumberOfChips) {
this.aggregateId = aggregateId;
this.gameId = gameId;
this.seatMap = seatMap;
this.chipsInBack = new HashMap<>();
seatMap.values().stream().filter(x -> x != null)
.forEach(x -> chipsInBack.put(x, startingNumberOfChips));
methodTable = new HashMap<>();
populateMethodTable();
if (!creatingFromEvents) {
TableCreatedEvent tableCreatedEvent = new TableCreatedEvent(
aggregateId, ++aggregateVersion, gameId, seatMap.size(),
seatMap, startingNumberOfChips);
addNewEvent(tableCreatedEvent);
applyCommonEvent(tableCreatedEvent);
}
}
@Override
public void applyAllHistoricalEvents(List<TableEvent> events) {
events.forEach(x -> {
aggregateVersion++;
applyCommonEvent(x);
});
}
private void populateMethodTable() {
methodTable.put(TableCreatedEvent.class, x -> {});
methodTable.put(CardsShuffledEvent.class, x -> {});
methodTable.put(HandDealtEvent.class, x -> applyHandDealtEvent((HandDealtEvent) x));
methodTable.put(AutoMoveHandForwardEvent.class, x -> {});
methodTable.put(PlayerCalledEvent.class, x -> currentHand.applyEvent((PlayerCalledEvent) x));
methodTable.put(PlayerCheckedEvent.class, x -> currentHand.applyEvent((PlayerCheckedEvent) x));
methodTable.put(PlayerForceCheckedEvent.class, x -> currentHand.applyEvent((PlayerForceCheckedEvent) x));
methodTable.put(PlayerFoldedEvent.class, x -> currentHand.applyEvent((PlayerFoldedEvent) x));
methodTable.put(PlayerForceFoldedEvent.class, x -> currentHand.applyEvent((PlayerForceFoldedEvent) x));
methodTable.put(PlayerRaisedEvent.class, x -> currentHand.applyEvent((PlayerRaisedEvent) x));
methodTable.put(FlopCardsDealtEvent.class, x -> currentHand.applyEvent((FlopCardsDealtEvent) x));
methodTable.put(TurnCardDealtEvent.class, x -> currentHand.applyEvent((TurnCardDealtEvent) x));
methodTable.put(RiverCardDealtEvent.class, x -> currentHand.applyEvent((RiverCardDealtEvent) x));
methodTable.put(PotAmountIncreasedEvent.class, x -> currentHand.applyEvent((PotAmountIncreasedEvent) x));
methodTable.put(PotClosedEvent.class, x -> currentHand.applyEvent((PotClosedEvent) x));
methodTable.put(PotCreatedEvent.class, x -> currentHand.applyEvent((PotCreatedEvent) x));
methodTable.put(RoundCompletedEvent.class, x -> currentHand.applyEvent((RoundCompletedEvent) x));
methodTable.put(ActionOnChangedEvent.class, x -> currentHand.applyEvent((ActionOnChangedEvent) x));
methodTable.put(LastToActChangedEvent.class, x -> currentHand.applyEvent((LastToActChangedEvent) x));
methodTable.put(WinnersDeterminedEvent.class, x -> currentHand.applyEvent((WinnersDeterminedEvent) x));
methodTable.put(HandCompletedEvent.class, x -> {
HandCompletedEvent event = (HandCompletedEvent) x;
currentHand = null;
chipsInBack.clear();
chipsInBack.putAll(new HashMap<>(event.getPlayerToChipsAtTableMap()));
});
methodTable.put(TablePausedEvent.class, x -> paused = true);
methodTable.put(TableResumedEvent.class, x -> paused = false);
methodTable.put(PlayerAddedEvent.class, x -> {
PlayerAddedEvent event = (PlayerAddedEvent) x;
seatMap.put(event.getPosition(), event.getPlayerId());
chipsInBack.put(event.getPlayerId(), event.getChipsInBack());
});
methodTable.put(PlayerRemovedEvent.class, x -> {
PlayerRemovedEvent event = (PlayerRemovedEvent) x;
Integer position = seatMap.entrySet().stream()
.filter(y -> y.getValue().equals(event.getPlayerId()))
.findFirst().get().getKey();
seatMap.remove(position);
chipsInBack.remove(event.getPlayerId());
});
methodTable.put(PlayerBustedTableEvent.class, x -> {
PlayerBustedTableEvent event = (PlayerBustedTableEvent) x;
Integer position = seatMap.entrySet().stream()
.filter(y -> y.getValue().equals(event.getPlayerId()))
.findFirst().get().getKey();
seatMap.remove(position);
chipsInBack.remove(event.getPlayerId());
});
}
private void applyCommonEvent(TableEvent event) {
methodTable.get(event.getClass()).applyEvent(event);
addAppliedEvent(event);
}
public UUID getAggregateId() {
return aggregateId;
}
private void applyHandDealtEvent(HandDealtEvent event) {
buttonOnPosition = event.getButtonOnPosition();
smallBlindPosition = event.getSmallBlindPosition();
bigBlindPosition = event.getBigBlindPosition();
currentHand = new Hand(event.getGameId(), event.getAggregateId(),
event.getHandId(), seatMap, event.getFlopCards(), event.getTurnCard(),
event.getRiverCard(), event.getButtonOnPosition(),
event.getSmallBlindPosition(), event.getBigBlindPosition(),
event.getLastToActPlayerId(), event.getPlayerToPocketCardsMap(),
event.getPossibleSeatActionsMap(), event.getPlayersStillInHand(),
event.getHandEvaluations(), event.getHandDealerState(),
event.getChipsInBack(), event.getChipsInFrontMap(),
event.getCallAmountsMap(), event.getRaiseToAmountsMap(),
event.getSmallBlind(), event.getBigBlind());
}
public void startNewHandForNewGame(int smallBlind, int bigBlind,
List<Card> shuffledDeckOfCards, CardsUsedInHand cardsUsedInHand,
Map<PocketCards, HandEvaluation> handEvaluations) {
buttonOnPosition = assignButtonOnForNewGame();
smallBlindPosition = assignSmallBlindForNewGame();
bigBlindPosition = assignBigBlindForNewGame();
int actionOnPosition = assignActionOnForNewHand();
performNewHandCommonLogic(smallBlind, bigBlind, shuffledDeckOfCards,
cardsUsedInHand, handEvaluations, actionOnPosition);
}
public void startNewHandForExistingTable(int smallBlind, int bigBlind,
List<Card> shuffledDeckOfCards, CardsUsedInHand cardsUsedInHand,
Map<PocketCards, HandEvaluation> handEvaluations) {
buttonOnPosition = assignButtonOnForNewHand();
smallBlindPosition = assignSmallBlindForNewHand();
bigBlindPosition = assignBigBlindForNewHand();
int actionOnPosition = assignActionOnForNewHand();
performNewHandCommonLogic(smallBlind, bigBlind, shuffledDeckOfCards,
cardsUsedInHand, handEvaluations, actionOnPosition);
}
private int assignButtonOnForNewGame() {
while (true) {
int potentialButtonOnPosition = new Random().nextInt(seatMap.size());
if (seatMap.get(Integer.valueOf(potentialButtonOnPosition)) != null) {
return potentialButtonOnPosition;
}
}
}
private int assignSmallBlindForNewGame() {
return getNumberOfPlayersAtTable() == 2 ? buttonOnPosition
: findNextFilledSeat(buttonOnPosition);
}
private int assignBigBlindForNewGame() {
return getNumberOfPlayersAtTable() == 2 ? findNextFilledSeat(buttonOnPosition)
: findNextFilledSeat(smallBlindPosition);
}
private int assignActionOnForNewHand() {
return findNextFilledSeat(bigBlindPosition);
}
private int assignBigBlindForNewHand() {
return findNextFilledSeat(bigBlindPosition);
}
private int assignSmallBlindForNewHand() {
return findNextFilledSeat(smallBlindPosition);
}
private int assignButtonOnForNewHand() {
return findNextFilledSeat(buttonOnPosition);
}
private int findNextFilledSeat(int startingPosition) {
for (int i = startingPosition + 1; i < seatMap.size(); i++) {
if (seatMap.get(Integer.valueOf(i)) != null) {
return i;
}
}
for (int i = 0; i < startingPosition; i++) {
if (seatMap.get(Integer.valueOf(i)) != null) {
return i;
}
}
throw new IllegalStateException("unable to find next filled seat");
}
private void performNewHandCommonLogic(int smallBlind, int bigBlind,
List<Card> shuffledDeckOfCards, CardsUsedInHand cardsUsedInHand,
Map<PocketCards, HandEvaluation> handEvaluations,
int actionOnPosition) {
CardsShuffledEvent cardsShuffledEvent = new CardsShuffledEvent(aggregateId,
++aggregateVersion, gameId, shuffledDeckOfCards);
addNewEvent(cardsShuffledEvent);
applyCommonEvent(cardsShuffledEvent);
int nextToReceivePocketCards = findNextFilledSeat(buttonOnPosition);
Map<UUID, PocketCards> playerToPocketCardsMap = new HashMap<>();
for (PocketCards pocketCards : cardsUsedInHand.getPocketCards()) {
UUID playerIdAtPosition = seatMap.get(Integer
.valueOf(nextToReceivePocketCards));
playerToPocketCardsMap.put(playerIdAtPosition, pocketCards);
nextToReceivePocketCards = findNextFilledSeat(nextToReceivePocketCards);
handEvaluations.get(pocketCards).setPlayerId(playerIdAtPosition);
}
Set<UUID> playersStillInHand = seatMap.values().stream().filter(x -> x != null)
.collect(Collectors.toSet());
Map<UUID, Set<PlayerAction>> possibleSeatActionsMap = new HashMap<>();
playersStillInHand.forEach(x -> possibleSeatActionsMap.put(x,
new HashSet<PlayerAction>()));
Hand hand = new Hand(gameId, aggregateId, UUID.randomUUID(), seatMap,
cardsUsedInHand.getFlopCards(), cardsUsedInHand.getTurnCard(),
cardsUsedInHand.getRiverCard(), buttonOnPosition, smallBlindPosition,
bigBlindPosition, null, playerToPocketCardsMap, possibleSeatActionsMap,
playersStillInHand, new ArrayList<>(handEvaluations.values()),
HandDealerState.NONE, chipsInBack, new HashMap<>(), new HashMap<>(),
new HashMap<>(), smallBlind, bigBlind);
List<TableEvent> eventsCreated = hand.dealHand(aggregateVersion + 1,
actionOnPosition);
eventsCreated.forEach(x -> {
addNewEvent(x);
applyCommonEvent(x);
aggregateVersion++;
});
}
public int getNumberOfPlayersAtTable() {
return (int) seatMap.values().stream().filter(x -> x != null).count();
}
public void check(UUID playerId) {
checkHandIsBeingPlayed();
TableEvent playerCheckedEvent = currentHand.check(playerId, false,
++aggregateVersion);
addNewEvent(playerCheckedEvent);
applyCommonEvent(playerCheckedEvent);
handleEndOfRound();
}
public void call(UUID playerId) {
checkHandIsBeingPlayed();
PlayerCalledEvent playerCalledEvent = currentHand.call(playerId,
++aggregateVersion);
addNewEvent(playerCalledEvent);
applyCommonEvent(playerCalledEvent);
handleEndOfRound();
}
public void fold(UUID playerId) {
checkHandIsBeingPlayed();
TableEvent playerFoldedEvent = currentHand.fold(playerId, false,
++aggregateVersion);
addNewEvent(playerFoldedEvent);
applyCommonEvent(playerFoldedEvent);
handleEndOfRound();
}
public void raise(UUID playerId, int raiseToAmount) {
checkHandIsBeingPlayed();
PlayerRaisedEvent playerRaisedEvent = currentHand.raise(playerId,
++aggregateVersion, raiseToAmount);
addNewEvent(playerRaisedEvent);
applyCommonEvent(playerRaisedEvent);
changeActionOnIfAppropriate();
}
public void expireActionOn(UUID handId, UUID playerId) {
checkHandIsBeingPlayed();
if (!currentHand.idMatches(handId)) {
return;
}
TableEvent forcedActionOnExpiredEvent = currentHand.expireActionOn(playerId,
++aggregateVersion);
addNewEvent(forcedActionOnExpiredEvent);
applyCommonEvent(forcedActionOnExpiredEvent);
handleEndOfRound();
}
public void autoMoveHandForward() {
checkHandIsBeingPlayed();
handleEndOfRound();
}
private void handleEndOfRound() {
handlePotAndRoundCompleted();
changeActionOnIfAppropriate();
dealCommonCardsIfAppropriate();
determineWinnersIfAppropriate();
removeAnyBustedPlayers();
finishHandIfAppropriate();
}
private void checkHandIsBeingPlayed() {
if (currentHand == null) {
throw new FlexPokerException("no hand in progress");
}
}
private void handlePotAndRoundCompleted() {
List<TableEvent> endOfRoundEvents = currentHand
.handlePotAndRoundCompleted(aggregateVersion + 1);
endOfRoundEvents.forEach(x -> {
addNewEvent(x);
// TODO: not using applyCommonEvent() here because PotHandler is too
// stateful and the events get applied to the state down there. when
// that's refactored, this should change
addAppliedEvent(x);
aggregateVersion++;
});
}
private void changeActionOnIfAppropriate() {
List<TableEvent> actionOnChangedEvents = currentHand
.changeActionOn(aggregateVersion + 1);
actionOnChangedEvents.forEach(x -> {
addNewEvent(x);
applyCommonEvent(x);
aggregateVersion++;
});
}
private void dealCommonCardsIfAppropriate() {
currentHand.dealCommonCardsIfAppropriate(aggregateVersion + 1).ifPresent(
event -> {
addNewEvent(event);
applyCommonEvent(event);
aggregateVersion++;
});
}
private void determineWinnersIfAppropriate() {
currentHand.determineWinnersIfAppropriate(aggregateVersion + 1).ifPresent(
event -> {
addNewEvent(event);
applyCommonEvent(event);
aggregateVersion++;
});
}
private void removeAnyBustedPlayers() {
chipsInBack.entrySet().stream()
.filter(x -> x.getValue() == 0)
.forEach(x -> {
PlayerBustedTableEvent event = new PlayerBustedTableEvent(aggregateId,
aggregateVersion, gameId, x.getKey());
addNewEvent(event);
applyCommonEvent(event);
aggregateVersion++;
});
}
private void finishHandIfAppropriate() {
Optional<TableEvent> handCompleteEvent = currentHand.finishHandIfAppropriate(aggregateVersion + 1);
if (handCompleteEvent.isPresent()) {
addNewEvent(handCompleteEvent.get());
applyCommonEvent(handCompleteEvent.get());
aggregateVersion++;
} else {
currentHand.autoMoveHandForward(aggregateVersion + 1).ifPresent(event -> {
addNewEvent(event);
applyCommonEvent(event);
aggregateVersion++;
});
}
}
public void addPlayer(UUID playerId, int chips) {
if (seatMap.values().contains(playerId)) {
throw new FlexPokerException("player already at this table");
}
int newPlayerPosition = findRandomOpenSeat();
PlayerAddedEvent playerAddedEvent = new PlayerAddedEvent(aggregateId,
++aggregateVersion, gameId, playerId, chips, newPlayerPosition);
addNewEvent(playerAddedEvent);
applyCommonEvent(playerAddedEvent);
}
public void removePlayer(UUID playerId) {
if (!seatMap.values().contains(playerId)) {
throw new FlexPokerException("player not at this table");
}
if (currentHand != null) {
throw new FlexPokerException("can't remove a player while in a hand");
}
PlayerRemovedEvent playerRemovedEvent = new PlayerRemovedEvent(aggregateId, ++aggregateVersion, gameId, playerId);
addNewEvent(playerRemovedEvent);
applyCommonEvent(playerRemovedEvent);
}
public void pause() {
if (paused) {
throw new FlexPokerException("table is already paused. can't pause again.");
}
TablePausedEvent tablePausedEvent = new TablePausedEvent(aggregateId, ++aggregateVersion, gameId);
addNewEvent(tablePausedEvent);
applyCommonEvent(tablePausedEvent);
}
public void resume() {
if (!paused) {
throw new FlexPokerException("table is not paused. can't resume.");
}
TableResumedEvent tableResumedEvent = new TableResumedEvent(aggregateId, ++aggregateVersion, gameId);
addNewEvent(tableResumedEvent);
applyCommonEvent(tableResumedEvent);
}
private int findRandomOpenSeat() {
while (true) {
int potentialNewPlayerPosition = new Random().nextInt(seatMap.size());
if (seatMap.get(potentialNewPlayerPosition) == null) {
return potentialNewPlayerPosition;
}
}
}
}