package com.flexpoker.game.command.aggregate; import java.util.Comparator; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import com.flexpoker.exception.FlexPokerException; 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; public class TableBalancer { private final int maxPlayersPerTable; private final UUID gameId; public TableBalancer(UUID gameId, int maxPlayersPerTable) { this.maxPlayersPerTable = maxPlayersPerTable; this.gameId = gameId; } public Optional<GameEvent> createSingleBalancingEvent(int version, UUID subjectTableId, Set<UUID> pausedTablesForBalancing, Map<UUID, Set<UUID>> tableToPlayersMap, Map<UUID, Integer> playerToChipsAtTableMap) { // make sure at least two players exists. we're in a bad state if not int totalNumberOfPlayers = getTotalNumberOfPlayers(tableToPlayersMap); if (totalNumberOfPlayers < 2) { throw new FlexPokerException( "the game should be over since less than two players are left. no balancing should be done"); } // remove any empty tables first Optional<Entry<UUID, Set<UUID>>> emptyTable = tableToPlayersMap .entrySet().stream().filter(x -> x.getValue().isEmpty()) .findFirst(); if (emptyTable.isPresent()) { return Optional.of(new TableRemovedEvent(gameId, version, emptyTable.get().getKey())); } // check to see if the number of tables is greater than the number it // should have. if so, move all the players from this table to other // tables starting with the one with the lowest number if (tableToPlayersMap .size() > getRequiredNumberOfTables(totalNumberOfPlayers)) { return createEventToMoveUserFromSubjectTableToAnyMinTable(version, subjectTableId, tableToPlayersMap, playerToChipsAtTableMap); } if (isTableOutOfBalance(subjectTableId, tableToPlayersMap)) { int tableSize = tableToPlayersMap.get(subjectTableId).size(); // in the min case, pause the table since it needs to get another // player if (tableSize == tableToPlayersMap.values().stream() .map(x -> x.size()).min(Integer::compare).get()) { return Optional.of(new TablePausedForBalancingEvent(gameId, version, subjectTableId)); } // only move a player if the out of balance table has the max number // of players. an out-of-balance medium-sized table should not send // any players if (tableSize == tableToPlayersMap.values().stream() .map(x -> x.size()).max(Integer::compare).get()) { return createEventToMoveUserFromSubjectTableToAnyMinTable( version, subjectTableId, tableToPlayersMap, playerToChipsAtTableMap); } } // re-examine the paused tables to see if they are still not balanced Set<UUID> pausedTablesThatShouldStayPaused = pausedTablesForBalancing .stream().filter(x -> isTableOutOfBalance(x, tableToPlayersMap)) .collect(Collectors.toSet()); // resume the first table that is currently paused, but are now balanced Optional<UUID> tableToResume = pausedTablesForBalancing.stream() .filter(x -> !pausedTablesThatShouldStayPaused.contains(x)) .findFirst(); if (tableToResume.isPresent()) { return Optional.of(new TableResumedAfterBalancingEvent(gameId, version, tableToResume.get())); } return Optional.empty(); } private Optional<GameEvent> createEventToMoveUserFromSubjectTableToAnyMinTable( int version, UUID subjectTableId, Map<UUID, Set<UUID>> tableToPlayersMap, Map<UUID, Integer> playerToChipsAtTableMap) { UUID playerToMove = tableToPlayersMap.get(subjectTableId).stream() .findFirst().get(); UUID toTableId = findNonSubjectMinTableId(subjectTableId, tableToPlayersMap); int chips = playerToChipsAtTableMap.get(playerToMove); return Optional.of(new PlayerMovedToNewTableEvent(gameId, version, subjectTableId, toTableId, playerToMove, chips)); } private UUID findNonSubjectMinTableId(UUID subjectTableId, Map<UUID, Set<UUID>> tableToPlayersMap) { return tableToPlayersMap.entrySet().stream() .filter(x -> !x.getKey().equals(subjectTableId)) .min(setSizeComparator()).get().getKey(); } private boolean isTableOutOfBalance(UUID tableId, Map<UUID, Set<UUID>> tableToPlayersMap) { int tableSize = tableToPlayersMap.get(tableId).size(); if (tableSize == 1) { return true; } return tableToPlayersMap.values().stream() // .anyMatch(x -> // (x.size() >= tableSize + 2) // || (x.size() <= tableSize - 2)); } private Comparator<? super Entry<UUID, Set<UUID>>> setSizeComparator() { return (x, y) -> Integer.compare(x.getValue().size(), y.getValue().size()); } private int getTotalNumberOfPlayers( Map<UUID, Set<UUID>> tableToPlayersMap) { return tableToPlayersMap.values().stream() .collect(Collectors.summingInt(Set::size)); } private int getRequiredNumberOfTables(int totalNumberOfPlayers) { int numberOfRequiredTables = totalNumberOfPlayers / maxPlayersPerTable; if (totalNumberOfPlayers % maxPlayersPerTable != 0) { numberOfRequiredTables++; } return numberOfRequiredTables; } }