Skip to content

Commit

Permalink
Add flexibility to voted pools (#1398)
Browse files Browse the repository at this point in the history
Signed-off-by: Pablo Herrera <[email protected]>
  • Loading branch information
Pablete1234 authored Sep 16, 2024
1 parent 50c2bc3 commit bf00343
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 166 deletions.
6 changes: 5 additions & 1 deletion core/src/main/java/tc/oc/pgm/PGMPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,11 @@ public void onEnable() {
if (config.getMapPoolFile() != null) {
MapPoolManager manager =
new MapPoolManager(logger, config.getMapPoolFile().toFile(), datastore);
if (manager.getActiveMapPool() != null) mapOrder = manager;
var pool = manager.getActiveMapPool();
if (pool != null) {
if (!pool.getMaps().isEmpty()) mapOrder = manager;
else logger.severe("Active pool has no maps. Falling back to a random pool.");
}
}
if (mapOrder == null) mapOrder = new RandomMapOrder(Lists.newArrayList(mapLibrary.getMaps()));

Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/tc/oc/pgm/api/map/MapOrder.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public interface MapOrder {
void setNextMap(MapInfo map);

/**
* Returns the duration used for cycles in {@link CycleMatchModule}.
* Returns the duration used for cycles in {@link tc.oc.pgm.cycle.CycleMatchModule}.
*
* @return The cycle duration
*/
Expand Down
82 changes: 59 additions & 23 deletions core/src/main/java/tc/oc/pgm/rotation/pools/VotingPool.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,24 @@
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import net.objecthunter.exp4j.ExpressionContext;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.MemoryConfiguration;
import tc.oc.pgm.api.map.MapInfo;
import tc.oc.pgm.api.match.Match;
import tc.oc.pgm.api.match.MatchScope;
import tc.oc.pgm.restart.RestartManager;
import tc.oc.pgm.rotation.MapPoolManager;
import tc.oc.pgm.rotation.vote.MapPoll;
import tc.oc.pgm.rotation.vote.MapVotePicker;
import tc.oc.pgm.util.math.Formula;

public class VotingPool extends MapPool {

// Arbitrary default of 1 in 5 players liking each map
public static final double DEFAULT_SCORE = 0.2;
// How much score to add/remove on a map every cycle
public final double ADJUST_FACTOR;

public final VoteConstants constants;
// The algorithm used to pick the maps for next vote.
public final MapVotePicker mapPicker;

Expand All @@ -35,11 +37,10 @@ public class VotingPool extends MapPool {
public VotingPool(
MapPoolType type, String name, MapPoolManager manager, ConfigurationSection section) {
super(type, name, manager, section);
this.constants = new VoteConstants(section, maps.size());

this.ADJUST_FACTOR = DEFAULT_SCORE / maps.size();

this.mapPicker = MapVotePicker.of(manager, section);
for (MapInfo map : maps) mapScores.put(map, DEFAULT_SCORE);
this.mapPicker = MapVotePicker.of(manager, constants, section);
for (MapInfo map : maps) mapScores.put(map, constants.defaultScore());
}

public VotingPool(
Expand All @@ -52,9 +53,9 @@ public VotingPool(
Duration cycleTime,
List<MapInfo> maps) {
super(type, name, manager, enabled, players, dynamic, cycleTime, maps);
this.ADJUST_FACTOR = DEFAULT_SCORE / maps.size();
this.mapPicker = MapVotePicker.of(manager, null);
for (MapInfo map : maps) mapScores.put(map, DEFAULT_SCORE);
this.constants = new VoteConstants(new MemoryConfiguration(), maps.size());
this.mapPicker = MapVotePicker.of(manager, constants, null);
for (MapInfo map : maps) mapScores.put(map, constants.defaultScore());
}

public MapPoll getCurrentPoll() {
Expand All @@ -66,23 +67,22 @@ public double getMapScore(MapInfo map) {
}

/** Ticks scores for all maps, making them go slowly towards DEFAULT_WEIGHT. */
private void tickScores(MapInfo currentMap) {
private void tickScores(Match match) {
// If the current map isn't from this pool, ignore ticking
if (!mapScores.containsKey(currentMap)) return;
mapScores.replaceAll(
(mapScores, value) ->
value > DEFAULT_SCORE
? Math.max(value - ADJUST_FACTOR, DEFAULT_SCORE)
: Math.min(value + ADJUST_FACTOR, DEFAULT_SCORE));
mapScores.put(currentMap, 0d);
if (!mapScores.containsKey(match.getMap())) return;
mapScores.replaceAll((mapScores, value) -> value > DEFAULT_SCORE
? Math.max(value - constants.scoreDecay(), DEFAULT_SCORE)
: Math.min(value + constants.scoreRise(), DEFAULT_SCORE));
mapScores.put(
match.getMap(), constants.scoreAfterPlay().applyAsDouble(new Context(match.getDuration())));
}

private void updateScores(Map<MapInfo, Set<UUID>> votes) {
double voters = votes.values().stream().flatMap(Collection::stream).distinct().count();
double voters =
votes.values().stream().flatMap(Collection::stream).distinct().count();
if (voters == 0) return; // Literally no one voted
votes.forEach(
(m, v) ->
mapScores.computeIfPresent(m, (a, b) -> Math.max(v.size() / voters, Double.MIN_VALUE)));
votes.forEach((m, v) ->
mapScores.computeIfPresent(m, (a, b) -> constants.afterVoteScore(v.size() / voters)));
}

@Override
Expand Down Expand Up @@ -111,12 +111,12 @@ public void setNextMap(MapInfo map) {

@Override
public void unloadPool(Match match) {
tickScores(match.getMap());
tickScores(match);
}

@Override
public void matchEnded(Match match) {
tickScores(match.getMap());
tickScores(match);
match
.getExecutor(MatchScope.LOADED)
.schedule(
Expand All @@ -132,4 +132,40 @@ public void matchEnded(Match match) {
5,
TimeUnit.SECONDS);
}

public record VoteConstants(
int voteOptions,
double defaultScore,
double scoreDecay,
double scoreRise,
double scoreAfterVoteMin,
double scoreAfterVoteMax,
double scoreMinToVote,
Formula<Context> scoreAfterPlay) {
private VoteConstants(ConfigurationSection section, int mapAmount) {
this(
section.getInt("vote-options", MapVotePicker.MAX_VOTE_OPTIONS), // Show 5 maps
section.getDouble("score.default", DEFAULT_SCORE), // Start at 20% each
section.getDouble("score.decay", DEFAULT_SCORE / mapAmount), // Proportional to # of maps
section.getDouble("score.rise", DEFAULT_SCORE / mapAmount), // Proportional to # of maps
section.getDouble("score.min-after-vote", 0.01), // min = 1%, never fully discard the map
section.getDouble("score.max-after-vote", 1), // max = 100%
section.getDouble("score.min-for-vote", 0.01), // To even be voted, need at least 1%
Formula.of(section.getString("score.after-playing"), Context.variables(), c -> 0));
}

public double afterVoteScore(double score) {
return Math.max(Math.min(score, scoreAfterVoteMax), scoreAfterVoteMin);
}
}

private static final class Context extends ExpressionContext.Impl {
public Context(Duration length) {
super(Map.of("play_minutes", length.toMillis() / 60_000d), null);
}

static Set<String> variables() {
return new Context(Duration.ZERO).getVariables();
}
}
}
84 changes: 39 additions & 45 deletions core/src/main/java/tc/oc/pgm/rotation/vote/MapVotePicker.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
package tc.oc.pgm.rotation.vote;

import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.stream.Collectors;
import net.objecthunter.exp4j.ExpressionContext;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.MemoryConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import tc.oc.pgm.api.PGM;
import tc.oc.pgm.api.map.Gamemode;
import tc.oc.pgm.api.map.MapInfo;
import tc.oc.pgm.api.map.MapTag;
import tc.oc.pgm.rotation.MapPoolManager;
import tc.oc.pgm.rotation.pools.VotingPool;
import tc.oc.pgm.util.math.Formula;
import tc.oc.pgm.util.math.VariableExpressionContext;

/**
* Responsible for picking the set of maps that will be on the vote. It's able to apply any
Expand All @@ -33,35 +33,39 @@ public class MapVotePicker {

// A 0 that prevents arbitrarily low values with tons of precision, which cause issues when mixed
// with larger numbers.
private static final double MINIMUM_WEIGHT = 0.00000001;
private static final double MINIMUM_WEIGHT = 0.000001;

private static final Formula<Context> DEFAULT_MODIFIER = c -> Math.pow(c.getVariable("score"), 2);
private static final Formula<MapVoteContext> DEFAULT_MODIFIER =
c -> Math.pow(c.getVariable("score"), 2);

private final MapPoolManager manager;
private final Formula<Context> modifier;
private final VotingPool.VoteConstants constants;
private final Formula<MapVoteContext> modifier;

public static MapVotePicker of(MapPoolManager manager, ConfigurationSection config) {
public static MapVotePicker of(
MapPoolManager manager, VotingPool.VoteConstants constants, ConfigurationSection config) {
// Create dummy config to read defaults off of.
if (config == null) config = new MemoryConfiguration();

Formula<Context> formula = DEFAULT_MODIFIER;
Formula<MapVoteContext> formula = DEFAULT_MODIFIER;
try {
formula =
Formula.of(
config.getString("modifier"),
Formula.ContextFactory.ofStatic(new Context().getVariables()),
DEFAULT_MODIFIER);
Formula.of(config.getString("modifier"), MapVoteContext.variables(), DEFAULT_MODIFIER);
} catch (IllegalArgumentException e) {
PGM.get()
.getLogger()
.log(Level.SEVERE, "Failed to load vote picker modifier formula, using fallback", e);
}

return new MapVotePicker(manager, formula);
return new MapVotePicker(manager, constants, formula);
}

public MapVotePicker(MapPoolManager manager, Formula<Context> modifier) {
private MapVotePicker(
MapPoolManager manager,
VotingPool.VoteConstants constants,
Formula<MapVoteContext> modifier) {
this.manager = manager;
this.constants = constants;
this.modifier = modifier;
}

Expand All @@ -84,7 +88,7 @@ protected List<MapInfo> getMaps(@Nullable List<MapInfo> selected, Map<MapInfo, D
if (selected == null) selected = new ArrayList<>();

List<MapInfo> unmodifiable = Collections.unmodifiableList(selected);
while (selected.size() < MAX_VOTE_OPTIONS) {
while (selected.size() < constants.voteOptions()) {
MapInfo map = getMap(unmodifiable, scores);

if (map == null) break; // Ran out of maps!
Expand Down Expand Up @@ -115,50 +119,40 @@ protected MapInfo getMap(List<MapInfo> selected, Map<MapInfo, Double> mapScores)
* @return random weight for the map
*/
public double getWeight(@Nullable List<MapInfo> selected, @NotNull MapInfo map, double score) {
if ((selected != null && selected.contains(map)) || score <= 0) return 0;
if ((selected != null && selected.contains(map)) || score <= constants.scoreMinToVote())
return 0;

Context context =
new Context(
score,
getRepeatedGamemodes(selected, map),
map.getMaxPlayers().stream().mapToInt(i -> i).sum(),
manager.getActivePlayers(null));
var context = new MapVoteContext(
score,
getRepeatedGamemodes(selected, map),
map.getMaxPlayers().stream().mapToInt(i -> i).sum(),
manager.getActivePlayers(null));

return Math.max(modifier.applyAsDouble(context), 0);
}

private double getRepeatedGamemodes(List<MapInfo> selected, MapInfo map) {
if (selected == null || selected.isEmpty()) return 0;
List<MapTag> gamemodes =
map.getTags().stream().filter(MapTag::isGamemode).collect(Collectors.toList());
Collection<Gamemode> gamemodes = map.getGamemodes();

return selected.stream().filter(s -> !Collections.disjoint(gamemodes, s.getTags())).count();
return selected.stream()
.filter(s -> !Collections.disjoint(gamemodes, s.getGamemodes()))
.count();
}

private static final class Context implements VariableExpressionContext {
private final ImmutableMap<String, Double> values;

public Context(double score, double sameGamemode, double mapsize, double players) {
this.values =
ImmutableMap.of(
private static final class MapVoteContext extends ExpressionContext.Impl {
public MapVoteContext(double score, double sameGamemode, double mapsize, double players) {
super(
Map.of(
"score", score,
"same_gamemode", sameGamemode,
"mapsize", mapsize,
"players", players);
}

private Context() {
this(0, 0, 0, 0);
}

@Override
public Set<String> getVariables() {
return values.keySet();
"players", players),
null);
}

@Override
public Double getVariable(String s) {
return values.get(s);
static Set<String> variables() {
return new MapVoteContext(0, 0, 0, 0).getVariables();
}
}
}
4 changes: 2 additions & 2 deletions core/src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ map:
# - uri: "https://github.com/PGMDev/Maps"
# path: "default-maps"

# A path to a map pools file, or empty to disable map pools.
pools: "map-pools.yml"
# A path to a map pools file, empty or commented-out to disable pools.
# pools: "map-pools.yml"

# A path to the includes folder, or empty to disable map includes.
includes: "includes"
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/resources/map-pools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ pools:
variants:
- default

# Voted pool only: num of maps to show each vote
vote-options: 5
# Voted pool only: modify how score behaves
score:
# How much score should maps start with by default.
default: 0.2
# How much score decreases/increases after each match. Null to be proportional to amount of maps.
decay: null
rise: null
# When a map is voted, enforce that it never falls out of the range of:
min-after-vote: 0.01 # 1%, avoids fully discarding the map
max-after-vote: 1
# Maps with less than this are ignored for a vote
min-for-vote: 0.01
# After the map plays, reset the score to:
# Supports a formula with play_minutes
after-playing: 0

# Voted pools support modifiers which come in the form of a formula (does not affect any other type of pool).
#
# This formula is parsed by exp4j library, it's quite flexible and supports many built-in functions.
Expand Down
Loading

0 comments on commit bf00343

Please sign in to comment.