Skip to content

Commit

Permalink
Add formulas to manage score to weight conversion
Browse files Browse the repository at this point in the history
Signed-off-by: Pablete1234 <[email protected]>
  • Loading branch information
Pablete1234 committed Jun 6, 2022
1 parent 4b1f264 commit 2b2a997
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 30 deletions.
3 changes: 1 addition & 2 deletions core/src/main/java/tc/oc/pgm/command/MapPoolCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ public static void pool(
if (chance && votes != null) {
double maxWeight = 0, currWeight;
for (MapInfo map : votes.getMaps()) {
chances.put(
map, currWeight = votes.mapPicker.getWeight(null, null, votes.getMapScore(map)));
chances.put(map, currWeight = votes.mapPicker.getWeight(null, map, votes.getMapScore(map)));
maxWeight += currWeight;
}
double finalMaxWeight = maxWeight;
Expand Down
16 changes: 14 additions & 2 deletions core/src/main/java/tc/oc/pgm/rotation/MapPoolManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -14,6 +15,7 @@
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.io.FileUtils;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
Expand Down Expand Up @@ -250,9 +252,19 @@ public void setNextMap(MapInfo map) {
getOrder().setNextMap(map);
}

public Optional<MapPool> getAppropriateDynamicPool(Match match) {
public double getActivePlayers(Match match) {
if (match == null) {
Iterator<Match> matches = PGM.get().getMatchManager().getMatches();
// Fallback to just raw online playercount
if (!matches.hasNext()) return Bukkit.getOnlinePlayers().size();
match = matches.next();
}
double obsBias = match.getModule(BlitzMatchModule.class) != null ? 0.85 : 0.5;
double activePlayers = match.getParticipants().size() + match.getObservers().size() * obsBias;
return match.getParticipants().size() + match.getObservers().size() * obsBias;
}

public Optional<MapPool> getAppropriateDynamicPool(Match match) {
double activePlayers = getActivePlayers(match);
return mapPools.keySet().stream()
.filter(MapPool::isDynamic)
.filter(pool -> activePlayers >= pool.getPlayers())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public VotingPool(MapPoolManager manager, ConfigurationSection section, String n

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

this.mapPicker = new MapVotePicker(section.getConfigurationSection("picker"));
this.mapPicker = MapVotePicker.of(manager, section);
for (MapInfo map : maps) mapScores.put(map, DEFAULT_SCORE);
}

Expand Down
158 changes: 158 additions & 0 deletions core/src/main/java/tc/oc/pgm/rotation/vote/Formula.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package tc.oc.pgm.rotation.vote;

import java.util.function.ToDoubleFunction;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import tc.oc.pgm.api.PGM;

/** Implementation of different formulas configurable & composable via config sections. */
public interface Formula extends ToDoubleFunction<Formula.Context> {

class Context {
double score;
double sameGamemode;
double mapsize;
double players;

public Context(double score, double sameGamemode, double mapsize, double players) {
this.score = score;
this.sameGamemode = sameGamemode;
this.mapsize = mapsize;
this.players = players;
}
}

Formula ZERO = constant(0);
Formula ONE = constant(1);
Formula POSITIVE_INFINITY = constant(Double.POSITIVE_INFINITY);
Formula NEGATIVE_INFINITY = constant(Double.NEGATIVE_INFINITY);

static Formula ofRequired(ConfigurationSection parent, String child)
throws InvalidConfigurationException {
Formula result = of(parent, child, null);
if (result == null)
throw new InvalidConfigurationException(
"Child '" + child + "' in '" + parent.getCurrentPath() + " is required");
return result;
}

static Formula of(ConfigurationSection parent, String child, Formula fallback)
throws InvalidConfigurationException {
if (parent == null) return fallback;
Object obj = parent.get(child);

if (obj == null) return fallback;
else if (obj instanceof Number) return constant(((Number) obj).doubleValue());
else if (obj instanceof String) return variable((String) obj);
else if (obj instanceof ConfigurationSection) return of((ConfigurationSection) obj);
else
throw new InvalidConfigurationException(
"Child '"
+ child
+ "' in '"
+ parent.getCurrentPath()
+ " must be null, double, or config section");
}

static Formula of(ConfigurationSection config) throws InvalidConfigurationException {
String type = config.getString("type").toLowerCase();
switch (type) {
case "constant":
return constant(config.getDouble("value"));
case "variable":
return variable(config.getString("value"));
case "sum":
return sum(
ofRequired(config, "a"),
ofRequired(config, "b"),
of(config, "c", ZERO),
of(config, "d", ZERO));
case "sub":
return sub(ofRequired(config, "a"), ofRequired(config, "b"));
case "mul":
return mul(
ofRequired(config, "a"),
ofRequired(config, "b"),
of(config, "c", ONE),
of(config, "d", ONE));
case "pow":
return pow(ofRequired(config, "value"), ofRequired(config, "exponent"));
case "bound":
return bound(
ofRequired(config, "value"),
of(config, "min", NEGATIVE_INFINITY),
of(config, "max", POSITIVE_INFINITY));
case "linear":
return linear(
ofRequired(config, "value"), of(config, "slope", ZERO), of(config, "offset", ZERO));
case "quadratic":
return quadratic(
ofRequired(config, "value"),
of(config, "arc", ZERO),
of(config, "slope", ZERO),
of(config, "offset", ZERO));
case "tanh":
return tanh(ofRequired(config, "value"));
default:
PGM.get()
.getLogger()
.severe("Invalid formula type for " + config.getCurrentPath() + ": '" + type + "'");
}
return null;
}

static Formula constant(double d) {
return x -> d;
}

static Formula variable(String str) throws InvalidConfigurationException {
switch (str) {
case "score":
return in -> in.score;
case "same_gamemode":
return in -> in.sameGamemode;
case "mapsize":
return in -> in.mapsize;
case "players":
return in -> in.players;
default:
throw new InvalidConfigurationException("Unknown variable type " + str);
}
}

static Formula sum(Formula a, Formula b, Formula c, Formula d) {
return x -> a.applyAsDouble(x) + b.applyAsDouble(x) + c.applyAsDouble(x) + d.applyAsDouble(x);
}

static Formula sub(Formula a, Formula b) {
return x -> a.applyAsDouble(x) - b.applyAsDouble(x);
}

static Formula mul(Formula a, Formula b, Formula c, Formula d) {
return x -> a.applyAsDouble(x) * b.applyAsDouble(x) * c.applyAsDouble(x) * d.applyAsDouble(x);
}

static Formula pow(Formula value, Formula exponent) {
return x -> Math.pow(value.applyAsDouble(x), exponent.applyAsDouble(x));
}

static Formula bound(Formula value, Formula min, Formula max) {
return x ->
Math.min(max.applyAsDouble(x), Math.max(min.applyAsDouble(x), value.applyAsDouble(x)));
}

static Formula linear(Formula val, Formula a, Formula b) {
return x -> a.applyAsDouble(x) * val.applyAsDouble(x) + b.applyAsDouble(x);
}

static Formula quadratic(Formula val, Formula a, Formula b, Formula c) {
return x -> {
double v = val.applyAsDouble(x);
return a.applyAsDouble(x) * v * v + b.applyAsDouble(x) * v + c.applyAsDouble(x);
};
}

static Formula tanh(Formula val) {
return x -> Math.tanh(val.applyAsDouble(x));
}
}
65 changes: 40 additions & 25 deletions core/src/main/java/tc/oc/pgm/rotation/vote/MapVotePicker.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.stream.Collectors;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
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.MapInfo;
import tc.oc.pgm.api.map.MapTag;
import tc.oc.pgm.rotation.MapPoolManager;

/**
* Responsible for picking the set of maps that will be on the vote. It's able to apply any
Expand All @@ -23,16 +28,28 @@ public class MapVotePicker {
public static final int MAX_VOTE_OPTIONS = 5;
public static final int MIN_CUSTOM_VOTE_OPTIONS = 2;

private final double gamemodeMultiplier;
private final double weightPower;
private static final Formula DEFAULT_MODIFIER = Formula.pow(c -> c.score, Formula.constant(2));

public MapVotePicker(ConfigurationSection config) {
private final MapPoolManager manager;
private final Formula modifier;

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

this.gamemodeMultiplier = config.getDouble("repeated-gamemode-multiplier", 1.0d);
this.weightPower = config.getDouble("weight-power", 2.0d);
// TODO: define format for online-playercount bias
Formula formula = DEFAULT_MODIFIER;
try {
formula = Formula.of(config, "modifier", DEFAULT_MODIFIER);
} catch (InvalidConfigurationException e) {
PGM.get().getLogger().log(Level.SEVERE, "Failed to load vote picker formula", e);
}

return new MapVotePicker(manager, formula);
}

public MapVotePicker(MapPoolManager manager, Formula modifier) {
this.manager = manager;
this.modifier = modifier;
}

/**
Expand Down Expand Up @@ -84,28 +101,26 @@ protected MapInfo getMap(List<MapInfo> selected, Map<MapInfo, Double> mapScores)
* @param score The score of the map, from player votes
* @return random weight for the map
*/
public double getWeight(@Nullable List<MapInfo> selected, @Nullable MapInfo map, double score) {
if (selected == null || map == null || selected.contains(map) || score <= 0) return 0;

double weight = score;
public double getWeight(@Nullable List<MapInfo> selected, @NotNull MapInfo map, double score) {
if ((selected != null && selected.contains(map)) || score <= 0) return 0;

// Remove score if same gamemode is already in the vote
if (gamemodeMultiplier != 1.0 && !selected.isEmpty()) {
List<MapTag> gamemodes =
map.getTags().stream().filter(MapTag::isGamemode).collect(Collectors.toList());

for (MapInfo otherMap : selected) {
if (!Collections.disjoint(gamemodes, otherMap.getTags())) weight *= gamemodeMultiplier;
}
}

// TODO: apply weight based on playercount

// Apply curve to bump up high weights and kill lower weights
weight = Math.pow(weight, weightPower);
Formula.Context context =
new Formula.Context(
score,
getRepeatedGamemodes(selected, map),
map.getMaxPlayers().stream().mapToInt(i -> i).sum(),
manager.getActivePlayers(null));

// Use MIN_VALUE so that weight isn't exactly 0.
// That allows for the map to be used if nothing else exists.
return Math.max(weight, Double.MIN_VALUE);
return Math.max(modifier.applyAsDouble(context), Double.MIN_VALUE);
}

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());

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

0 comments on commit 2b2a997

Please sign in to comment.