package com.flexpoker.game.command.aggregate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.flexpoker.exception.FlexPokerException;
import com.flexpoker.framework.command.EventApplier;
import com.flexpoker.framework.domain.AggregateRoot;
import com.flexpoker.game.command.events.BlindsIncreasedEvent;
import com.flexpoker.game.command.events.GameCreatedEvent;
import com.flexpoker.game.command.events.GameFinishedEvent;
import com.flexpoker.game.command.events.GameJoinedEvent;
import com.flexpoker.game.command.events.GameMovedToStartingStageEvent;
import com.flexpoker.game.command.events.GameStartedEvent;
import com.flexpoker.game.command.events.GameTablesCreatedAndPlayersAssociatedEvent;
import com.flexpoker.game.command.events.NewHandIsClearedToStartEvent;
import com.flexpoker.game.command.events.PlayerBustedGameEvent;
import com.flexpoker.game.command.events.PlayerMovedToNewTableEvent;
import com.flexpoker.game.command.events.TablePausedForBalancingEvent;
import com.flexpoker.game.command.events.TableRemovedEvent;
import com.flexpoker.game.command.events.TableResumedAfterBalancingEvent;
import com.flexpoker.game.command.framework.GameEvent;
import com.flexpoker.game.query.dto.GameStage;
public class Game extends AggregateRoot<GameEvent> {
private final UUID aggregateId;
private int aggregateVersion;
private final Map<Class<? extends GameEvent>, EventApplier<? super GameEvent>> methodTable;
private GameStage gameStage;
private final Set<UUID> registeredPlayerIds;
private final int maxNumberOfPlayers;
private final int numberOfPlayersPerTable;
private final Map<UUID, Set<UUID>> tableIdToPlayerIdsMap;
private final BlindSchedule blindSchedule;
private final TableBalancer tableBalancer;
private final Set<UUID> pausedTablesForBalancing;
protected Game(boolean creatingFromEvents, UUID aggregateId,
String gameName, int maxNumberOfPlayers, int numberOfPlayersPerTable,
UUID createdById, GameStage gameStage, BlindSchedule blindSchedule,
TableBalancer tableBalancer) {
this.aggregateId = aggregateId;
this.maxNumberOfPlayers = maxNumberOfPlayers;
this.numberOfPlayersPerTable = numberOfPlayersPerTable;
this.blindSchedule = blindSchedule;
this.tableBalancer = tableBalancer;
this.gameStage = gameStage;
registeredPlayerIds = new HashSet<>();
tableIdToPlayerIdsMap = new HashMap<>();
pausedTablesForBalancing = new HashSet<>();
methodTable = new HashMap<>();
populateMethodTable();
if (!creatingFromEvents) {
GameCreatedEvent gameCreatedEvent = new GameCreatedEvent(aggregateId,
++aggregateVersion, gameName, maxNumberOfPlayers,
numberOfPlayersPerTable, createdById,
blindSchedule.getNumberOfMinutesBetweenLevels());
addNewEvent(gameCreatedEvent);
applyCommonEvent(gameCreatedEvent);
}
}
@Override
public void applyAllHistoricalEvents(List<GameEvent> events) {
for (GameEvent event : events) {
aggregateVersion++;
applyCommonEvent(event);
}
}
private void populateMethodTable() {
methodTable.put(GameCreatedEvent.class, x -> {});
methodTable.put(GameJoinedEvent.class, x -> registeredPlayerIds
.add(((GameJoinedEvent) x).getPlayerId()));
methodTable.put(GameMovedToStartingStageEvent.class, x -> gameStage = GameStage.STARTING);
methodTable.put(GameStartedEvent.class, x -> gameStage = GameStage.INPROGRESS);
methodTable.put(GameTablesCreatedAndPlayersAssociatedEvent.class,
x -> tableIdToPlayerIdsMap
.putAll(((GameTablesCreatedAndPlayersAssociatedEvent) x)
.getTableIdToPlayerIdsMap()));
methodTable.put(GameFinishedEvent.class, x -> gameStage = GameStage.FINISHED);
methodTable.put(NewHandIsClearedToStartEvent.class, x -> {});
methodTable.put(BlindsIncreasedEvent.class, x -> blindSchedule.incrementLevel());
methodTable.put(TableRemovedEvent.class, x -> {
UUID tableId = ((TableRemovedEvent) x).getTableId();
tableIdToPlayerIdsMap.remove(tableId);
pausedTablesForBalancing.remove(tableId);
});
methodTable.put(TablePausedForBalancingEvent.class, x -> {
UUID tableId = ((TablePausedForBalancingEvent) x).getTableId();
pausedTablesForBalancing.add(tableId);
});
methodTable.put(TableResumedAfterBalancingEvent.class, x -> {
UUID tableId = ((TableResumedAfterBalancingEvent) x).getTableId();
pausedTablesForBalancing.remove(tableId);
});
methodTable.put(PlayerMovedToNewTableEvent.class, x -> {
PlayerMovedToNewTableEvent event = (PlayerMovedToNewTableEvent) x;
tableIdToPlayerIdsMap.get(event.getFromTableId()).remove(event.getPlayerId());
tableIdToPlayerIdsMap.get(event.getToTableId()).add(event.getPlayerId());
});
methodTable.put(PlayerBustedGameEvent.class, x -> {
PlayerBustedGameEvent event = (PlayerBustedGameEvent) x;
UUID tableId = tableIdToPlayerIdsMap.entrySet().stream()
.filter(y -> y.getValue().contains(event.getPlayerId()))
.findAny().get().getKey();
tableIdToPlayerIdsMap.get(tableId).remove(event.getPlayerId());
});
}
private void applyCommonEvent(GameEvent event) {
methodTable.get(event.getClass()).applyEvent(event);
addAppliedEvent(event);
}
public void joinGame(UUID playerId) {
createJoinGameEvent(playerId);
// if the game is at the max capacity, start the game
if (registeredPlayerIds.size() == maxNumberOfPlayers) {
createGameMovedToStartingStageEvent();
createGameTablesCreatedAndPlayersAssociatedEvent();
createGameStartedEvent();
}
}
public void attemptToStartNewHand(UUID tableId, Map<UUID, Integer> playerToChipsAtTableMap) {
if (gameStage != GameStage.INPROGRESS) {
throw new FlexPokerException("the game must be INPROGRESS if trying"
+ "to start a new hand");
}
playerToChipsAtTableMap.entrySet().stream()
.filter(x -> x.getValue() == 0)
.map(x -> x.getKey())
.forEach(x -> bustPlayer(x));
Set<UUID> bustedPlayers = tableIdToPlayerIdsMap.get(tableId).stream()
.filter(x -> !playerToChipsAtTableMap.keySet().contains(x))
.collect(Collectors.toSet());
bustedPlayers.forEach(x -> bustPlayer(x));
if (tableIdToPlayerIdsMap.values().stream().flatMap(Collection::stream).count() == 1) {
// TODO: do something for the winner
} else {
Optional<GameEvent> singleBalancingEvent;
do {
singleBalancingEvent = tableBalancer.createSingleBalancingEvent(aggregateVersion + 1,
tableId, pausedTablesForBalancing, tableIdToPlayerIdsMap, playerToChipsAtTableMap);
if (singleBalancingEvent.isPresent()) {
aggregateVersion++;
addNewEvent(singleBalancingEvent.get());
applyCommonEvent(singleBalancingEvent.get());
}
} while (singleBalancingEvent.isPresent());
if (tableIdToPlayerIdsMap.containsKey(tableId) && !pausedTablesForBalancing.contains(tableId)) {
NewHandIsClearedToStartEvent event = new NewHandIsClearedToStartEvent(
aggregateId, ++aggregateVersion, tableId,
blindSchedule.getCurrentBlindAmounts());
addNewEvent(event);
applyCommonEvent(event);
}
}
}
public void increaseBlinds() {
if (gameStage != GameStage.INPROGRESS) {
throw new FlexPokerException(
"cannot increase blinds if the game isn't in progress");
}
if (!blindSchedule.isMaxLevel()) {
BlindsIncreasedEvent event = new BlindsIncreasedEvent(aggregateId, ++aggregateVersion);
addNewEvent(event);
applyCommonEvent(event);
}
}
private void bustPlayer(UUID playerId) {
if (tableIdToPlayerIdsMap.values().stream()
.flatMap(Collection::stream)
.noneMatch(x -> x.equals(playerId))) {
throw new FlexPokerException("player is not active in the game");
}
PlayerBustedGameEvent event = new PlayerBustedGameEvent(aggregateId, ++aggregateVersion, playerId);
addNewEvent(event);
applyCommonEvent(event);
}
private void createJoinGameEvent(UUID playerId) {
if (gameStage == GameStage.STARTING || gameStage == GameStage.INPROGRESS) {
throw new FlexPokerException("The game has already started");
}
if (gameStage == GameStage.FINISHED) {
throw new FlexPokerException("The game is already finished.");
}
if (registeredPlayerIds.contains(playerId)) {
throw new FlexPokerException("You are already in this game.");
}
GameJoinedEvent event = new GameJoinedEvent(aggregateId, ++aggregateVersion,
playerId);
addNewEvent(event);
applyCommonEvent(event);
}
private void createGameMovedToStartingStageEvent() {
if (gameStage != GameStage.REGISTERING) {
throw new FlexPokerException(
"to move to STARTING, the game stage must be REGISTERING");
}
GameMovedToStartingStageEvent event = new GameMovedToStartingStageEvent(
aggregateId, ++aggregateVersion);
addNewEvent(event);
applyCommonEvent(event);
}
private void createGameTablesCreatedAndPlayersAssociatedEvent() {
if (!tableIdToPlayerIdsMap.isEmpty()) {
throw new FlexPokerException(
"tableToPlayerIdsMap should be empty when initializing the tables");
}
Map<UUID, Set<UUID>> tableIdToPlayerIdsMap = createTableToPlayerMap();
GameTablesCreatedAndPlayersAssociatedEvent event = new GameTablesCreatedAndPlayersAssociatedEvent(
aggregateId, ++aggregateVersion, tableIdToPlayerIdsMap,
numberOfPlayersPerTable);
addNewEvent(event);
applyCommonEvent(event);
}
private Map<UUID, Set<UUID>> createTableToPlayerMap() {
List<UUID> randomizedListOfPlayerIds = new ArrayList<>(registeredPlayerIds);
Collections.shuffle(randomizedListOfPlayerIds);
Map<UUID, Set<UUID>> tableIdToPlayerIdsMap = new HashMap<>();
int numberOfTablesToCreate = determineNumberOfTablesToCreate();
Stream.iterate(0, e -> e)
.limit(numberOfTablesToCreate)
.forEach(x -> tableIdToPlayerIdsMap.put(UUID.randomUUID(), new HashSet<>()));
List<UUID> tableIdList = new ArrayList<>(tableIdToPlayerIdsMap.keySet());
Stream.iterate(0, e -> e + 1)
.limit(randomizedListOfPlayerIds.size())
.forEach(x -> {
int tableIndex = x % tableIdList.size();
tableIdToPlayerIdsMap.get(tableIdList.get(tableIndex))
.add(randomizedListOfPlayerIds.get(x));
});
return tableIdToPlayerIdsMap;
}
private int determineNumberOfTablesToCreate() {
int numberOfTables = registeredPlayerIds.size() / numberOfPlayersPerTable;
// if the number of people doesn't fit perfectly, then an additional
// table is needed for the overflow
if (registeredPlayerIds.size() % numberOfPlayersPerTable != 0) {
numberOfTables++;
}
return numberOfTables;
}
private void createGameStartedEvent() {
if (gameStage != GameStage.STARTING) {
throw new FlexPokerException(
"to move to STARTED, the game stage must be STARTING");
}
if (tableIdToPlayerIdsMap.isEmpty()) {
throw new FlexPokerException(
"tableToPlayerIdsMap should be filled at this point");
}
GameStartedEvent event = new GameStartedEvent(aggregateId,
++aggregateVersion, tableIdToPlayerIdsMap.keySet(),
blindSchedule);
addNewEvent(event);
applyCommonEvent(event);
}
}