package net.anxuiz.tourney.vote; import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; import javax.inject.Inject; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.sk89q.minecraft.util.commands.ChatColor; import net.anxuiz.tourney.Config; import net.anxuiz.tourney.MapClassification; import net.anxuiz.tourney.TeamManager; import net.anxuiz.tourney.Tourney; import net.anxuiz.tourney.TourneyState; import net.anxuiz.tourney.event.mapselect.MapSelectionBeginEvent; import net.anxuiz.tourney.event.mapselect.MapSelectionClassificationSelectEvent; import net.anxuiz.tourney.event.mapselect.MapSelectionClassificationVetoEvent; import net.anxuiz.tourney.event.mapselect.MapSelectionMapSelectEvent; import net.anxuiz.tourney.event.mapselect.MapSelectionMapVetoEvent; import net.anxuiz.tourney.event.mapselect.MapSelectionTurnCycleEvent; import net.anxuiz.tourney.util.EntrantUtils; import org.apache.commons.collections.CollectionUtils; import org.bukkit.Server; import org.bukkit.event.EventBus; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import tc.oc.api.docs.Entrant; import tc.oc.commons.core.chat.Audience; import tc.oc.commons.core.formatting.StringUtils; import tc.oc.pgm.cycle.CycleMatchModule; import tc.oc.pgm.events.CycleEvent; import tc.oc.pgm.map.PGMMap; import tc.oc.pgm.match.Match; import tc.oc.pgm.match.inject.MatchScoped; @MatchScoped public class VoteContext { private final Tourney tourney; private final EntrantUtils entrantUtils; private final TeamManager teamManager; private final Match match; private final EventBus eventBus; private final Server bukkit; private VetoVote vote; private VoteListener listener; @Inject VoteContext(Tourney tourney, EntrantUtils entrantUtils, TeamManager teamManager, Match match, EventBus eventBus, Server bukkit) { this.tourney = tourney; this.entrantUtils = entrantUtils; this.teamManager = teamManager; this.match = match; this.eventBus = eventBus; this.bukkit = bukkit; this.vote = null; } public Optional<VetoVote> currentVote() { return Optional.ofNullable(vote); } public boolean voteInProgress() { return vote != null && (vote.getSelectedClassification() == null || vote.getSelectedMap() == null); } public void clearCurrentVote() { if(listener != null) { match.unregisterEvents(listener); listener = null; vote = null; } } public void startVote(Collection<MapClassification> classifications) { vote = new VetoVoteImpl(classifications); listener = new VoteListener(); match.registerEvents(listener); eventBus.callEvent(new MapSelectionBeginEvent(vote)); vote.cycleTurn(); } private class VoteListener implements Listener { @EventHandler public void onMapSelectionBegin(MapSelectionBeginEvent event) { bukkit.broadcastMessage(ChatColor.YELLOW + "Beginning map selection vote..."); tourney.setState(TourneyState.ENABLED_MAP_SELECTION); } @EventHandler public void onClassificationVeto(MapSelectionClassificationVetoEvent event) { bukkit.broadcastMessage( event.getTeam().getColoredName() + ChatColor.RESET + ChatColor.YELLOW + " vetoed a classification"); } @EventHandler public void onMapVeto(MapSelectionMapVetoEvent event) { bukkit.broadcastMessage( event.getTeam().getColoredName() + ChatColor.RESET + ChatColor.YELLOW + " vetoed a map"); } @EventHandler public void onMapSelect(MapSelectionMapSelectEvent event) { bukkit.broadcastMessage(ChatColor.YELLOW + "The winning map has been selected: " + ChatColor.AQUA + event.getMap().getName()); match.needMatchModule(CycleMatchModule.class) .startCountdown(Duration.ofSeconds(15), event.getMap()); } @EventHandler public void onClassificationSelect(MapSelectionClassificationSelectEvent event) { bukkit.broadcastMessage(ChatColor.YELLOW + "The winning classification has been selected: " + ChatColor.AQUA + event.getClassification()); } @EventHandler public void onTurnCycle(MapSelectionTurnCycleEvent event) { VetoVote vote = event.getVote(); boolean classificationSelected = vote.getSelectedClassification() != null; // broadcast to match bukkit.broadcastMessage(ChatColor.YELLOW + "Waiting for both teams to veto a " + (classificationSelected ? "map" : "classification") + "..." ); // broadcast to team if (classificationSelected) { for (Entrant participation : vote.getParticipatingTeams()) { final Audience team = teamManager.entrantToTeam(participation).audience(); team.sendMessage(StringUtils.dashedChatMessage(ChatColor.GRAY + " Veto Information", "-", ChatColor.RED + "" + ChatColor.STRIKETHROUGH)); team.sendMessage(ChatColor.GRAY + "Your team may now veto another map."); team.sendMessage(ChatColor.GRAY + "Note that any team member may veto. Please consult with your teammates and choose wisely."); team.sendMessage(ChatColor.GRAY + "Execute " + ChatColor.RESET + ChatColor.YELLOW + ChatColor.ITALIC + "/tourney map options [page]" + ChatColor.RESET + ChatColor.GRAY + " to display remaining maps."); team.sendMessage(ChatColor.GRAY + "Execute " + ChatColor.RESET + ChatColor.YELLOW + ChatColor.ITALIC + "/tourney map veto <map...>" + ChatColor.RESET + ChatColor.GRAY + " to cast your team's veto."); team.sendMessage(StringUtils.dashedChatMessage(ChatColor.RED + "" + ChatColor.STRIKETHROUGH + "----------------", "-", ChatColor.RED + "" + ChatColor.STRIKETHROUGH)); } } else { for (Entrant participation : vote.getParticipatingTeams()) { final Audience team = teamManager.entrantToTeam(participation).audience(); team.sendMessage(StringUtils.dashedChatMessage(ChatColor.GRAY + " Veto Information", "-", ChatColor.RED + "" + ChatColor.STRIKETHROUGH)); team.sendMessage(ChatColor.GRAY + "Your team may now veto another classification."); team.sendMessage(ChatColor.GRAY + "Note that any team member may veto. Please consult with your teammates and choose wisely."); team.sendMessage(ChatColor.GRAY + "Execute " + ChatColor.RESET + ChatColor.YELLOW + ChatColor.ITALIC + "/tourney map options [page]" + ChatColor.RESET + ChatColor.GRAY + " to display remaining classifications."); team.sendMessage(ChatColor.GRAY + "Execute " + ChatColor.RESET + ChatColor.YELLOW + ChatColor.ITALIC + "/tourney map veto <classification...>" + ChatColor.RESET + ChatColor.GRAY + " to cast your team's veto."); team.sendMessage(StringUtils.dashedChatMessage(ChatColor.RED + "" + ChatColor.STRIKETHROUGH + "----------------", "-", ChatColor.RED + "" + ChatColor.STRIKETHROUGH)); } } } @EventHandler(priority = EventPriority.HIGH) public void onMatchCycle(CycleEvent event) { tourney.setState(TourneyState.ENABLED_WAITING_FOR_TEAMS); teamManager.assignTeams(vote.getParticipatingTeams()); clearCurrentVote(); } } /** Represents a veto vote. */ private class VetoVoteImpl implements VetoVote { private final Collection<Entrant> participatingTeams; private final HashSet<MapClassification> remainingClassifications = new HashSet<>(); private final HashSet<PGMMap> remainingMaps = new HashSet<>(); private Collection<Entrant> currentTurnRemainingTeams; private Collection<MapClassification> currentTurnVetoedClassifications; private Collection<PGMMap> currentTurnVetoedMaps; public VetoVoteImpl(Collection<MapClassification> classifications) { this.participatingTeams = teamManager.getEntrants(); HashSet<MapClassification> excludedClassifications = new HashSet<>(); HashSet<PGMMap> excludedMaps = new HashSet<>(); for (Entrant entrant : participatingTeams) { HashMap<MapClassification, Integer> classificationCounts = new HashMap<>(); for (MapClassification classification : classifications) { classificationCounts.put(classification, entrantUtils.getClassificationPlayCount(entrant, classification)); } // remove classifications that have been played X times more than any other classification int maxClassificationPlays = Config.maxClassificationPlays(); for (Map.Entry<MapClassification, Integer> entry : classificationCounts.entrySet()) { int value = entry.getValue(); if (value > 0) { for (int count : classificationCounts.values()) { if ((count > 0 && count * maxClassificationPlays <= value) || (count == 0 && value >= maxClassificationPlays)) { MapClassification classification = entry.getKey(); excludedClassifications.add(classification); excludedMaps.addAll(classification.maps()); } } } } HashMap<PGMMap, Integer> mapCounts = new HashMap<>(); for (MapClassification classification : classifications) { for (PGMMap map : classification.maps()) { mapCounts.put(map, entrantUtils.getMapPlayCount(entrant, map)); } } // exclude maps that have been played X times more than any other map int maxMapPlays = Config.maxMapPlays(); for (Map.Entry<PGMMap, Integer> entry : mapCounts.entrySet()) { int value = entry.getValue(); if (value > 0) { for (int count : mapCounts.values()) { if ((count > 0 && count * maxMapPlays <= value) || (count == 0 && value >= maxMapPlays)) { excludedMaps.add(entry.getKey()); } } } } } // remove excluded classifications, and reset filtering if no classifications remain this.remainingClassifications.addAll(CollectionUtils.subtract(classifications, excludedClassifications)); if (this.remainingClassifications.isEmpty()) { this.remainingClassifications.addAll(classifications); } // remove excluded maps, and reset filtering if no maps remain for (MapClassification classification : classifications) { this.remainingMaps.addAll(CollectionUtils.subtract(classification.maps(), excludedMaps)); } if (this.remainingMaps.isEmpty()) { for (MapClassification classification : classifications) { this.remainingMaps.addAll(classification.maps()); } } // remove maps that have no classifications mapLoop: for (PGMMap map : new HashSet<>(this.remainingMaps)) { for (MapClassification classification : this.remainingClassifications) { /* We can safely ignore maps in the classification that aren't included, because they will already have ~ been excluded by this point. */ if (classification.maps().contains(map)) break mapLoop; } this.remainingMaps.remove(map); } // remove classifications that have no maps for (MapClassification classification : new HashSet<>(this.remainingClassifications)) { if (Collections.disjoint(classification.maps(), this.remainingMaps)) { this.remainingClassifications.remove(classification); } } } @Override public Collection<Entrant> getCurrentTurnRemainingTeams() { return this.currentTurnRemainingTeams; } @Override public ImmutableSet<MapClassification> getRemainingClassifications() { return ImmutableSet.copyOf(this.remainingClassifications); } @Override public ImmutableSet<PGMMap> getRemainingMaps() { return ImmutableSet.copyOf(this.remainingMaps); } @Override public Collection<Entrant> getParticipatingTeams() { return participatingTeams; } @Override public @Nullable MapClassification getSelectedClassification() { int remaining = this.remainingClassifications.size(); Preconditions.checkState(remaining > 0, "Remaining classification count was not greater than 0"); if (remaining < 3) { // return least played if (remaining == 2) { MapClassification winningClassification = null; int winningCount = Integer.MAX_VALUE; for (MapClassification classification : this.remainingClassifications) { int count = 0; for (Entrant entrant : this.participatingTeams) { count += entrantUtils.getClassificationPlayCount(entrant, classification); } if (count <= winningCount) { winningClassification = classification; winningCount = count; } } return winningClassification; } else /* remaining == 1 */ { return this.remainingClassifications.iterator().next(); } } return null; } @Override public @Nullable PGMMap getSelectedMap() { int remaining = this.remainingMaps.size(); Preconditions.checkState(remaining > 0, "Remaining map count was not greater than 0"); if (remaining < 3) { // return least played if (remaining == 2) { PGMMap winningMap = null; int winningCount = 0; for (PGMMap map : this.remainingMaps) { int count = 0; for (Entrant entrant : this.participatingTeams) { count += entrantUtils.getMapPlayCount(entrant, map); } if (count <= winningCount) { winningMap = map; winningCount = count; } } return winningMap; } else /* remaining == 1 */ { return this.remainingMaps.iterator().next(); } } return null; } @Override public void registerVeto(Entrant entrant, PGMMap map) { Preconditions.checkNotNull(entrant, "Entrant"); Preconditions.checkArgument(this.remainingMaps.contains(Preconditions.checkNotNull(map, "Map")), "Map specified to veto was not remaining"); this.currentTurnVetoedMaps.add(map); this.currentTurnRemainingTeams.remove(entrant); eventBus.callEvent(new MapSelectionMapVetoEvent(this, teamManager.entrantToTeam(entrant), entrant)); if (this.currentTurnRemainingTeams.isEmpty()) { this.remainingMaps.removeAll(this.currentTurnVetoedMaps); PGMMap winnerMap = this.getSelectedMap(); if (winnerMap != null) { this.remainingMaps.clear(); this.remainingMaps.add(winnerMap); eventBus.callEvent(new MapSelectionMapSelectEvent(this, winnerMap)); } else { this.cycleTurn(); } } } @Override public void registerVeto(Entrant entrant, MapClassification classification) { Preconditions.checkNotNull(entrant, "Entrant"); Preconditions.checkArgument(this.remainingClassifications.contains(Preconditions.checkNotNull(classification, "Classification")), "Classification specified to veto was not remaining"); this.currentTurnVetoedClassifications.add(classification); this.currentTurnRemainingTeams.remove(entrant); eventBus.callEvent(new MapSelectionClassificationVetoEvent(this, teamManager.entrantToTeam(entrant), entrant)); if (this.currentTurnRemainingTeams.isEmpty()) { this.remainingClassifications.removeAll(this.currentTurnVetoedClassifications); for (MapClassification cl : this.currentTurnVetoedClassifications) { this.remainingMaps.removeAll(cl.maps()); } MapClassification winnerClassification = this.getSelectedClassification(); if (winnerClassification != null) { this.remainingClassifications.clear(); this.remainingClassifications.add(winnerClassification); for (PGMMap map : new HashSet<>(this.remainingMaps)) { if (!winnerClassification.maps().contains(map)) { this.remainingMaps.remove(map); } } eventBus.callEvent(new MapSelectionClassificationSelectEvent(this, winnerClassification)); } PGMMap winnerMap = this.getSelectedMap(); if (winnerMap != null) { this.remainingMaps.clear(); this.remainingMaps.add(winnerMap); eventBus.callEvent(new MapSelectionMapSelectEvent(this, winnerMap)); } else { this.cycleTurn(); } } } @Override public void cycleTurn() { eventBus.callEvent(new MapSelectionTurnCycleEvent(this)); this.currentTurnVetoedClassifications = null; this.currentTurnVetoedMaps = null; this.currentTurnRemainingTeams = new HashSet<>(this.participatingTeams); if (this.getSelectedClassification() != null) { this.currentTurnVetoedMaps = new HashSet<>(); } else { this.currentTurnVetoedClassifications = new HashSet<>(); } } } }