mirror of
https://github.com/garbagemule/MobArena.git
synced 2024-11-26 04:25:21 +01:00
Add support for custom formulas.
This commit re-frames the formula concept used by the wave growth, swarm amount, and boss health wave configuration properties. It fundamentally changes how these values are calculated, from a static, compile-time set of enum values and hardcoded expressions, to a powerful math expression feature that supports constants, variables, operators, and functions. In part to remain backwards compatible with existing MobArena setups, and in part for a better user experience, the old enum-based expressions are relocated into a new file, `formulas.yml`, as _macros_. The file is written to the plugin folder if missing, and it contains a formula for each of the legacy values for each of the enums. Additionally, it has a global section with some predefined macros for inspiration's sake. The goal of this file is to allow people to define new formulas and reuse them in their wave configurations instead of having to duplicate the same formulas again and again. Parts of the system are extensible. It is possible for other plugins to register additional constants, variables, operators, and functions. Closes #460 Closes #461
This commit is contained in:
parent
0da90f3963
commit
9081ec8055
@ -11,6 +11,9 @@ These changes will (most likely) be included in the next version.
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- It is now possible to write custom formulas for wave growth in Default Wave, swarm amounts in Swarm Waves, and boss health in Boss Waves, allowing for much more control and fine-tuning. The formulas support various session-related variables as well as various mathematical operators and functions. Formulas can be predefined as macros in the new `formulas.yml` file. Check the wiki for details.
|
||||
|
||||
### Fixed
|
||||
- Arena signs in unloaded or missing worlds no longer break the startup procedure. Sign data is stored in a new format that MobArena will automatically migrate to on a per-world basis during startup.
|
||||
|
||||
|
@ -248,7 +248,7 @@ public class MASpawnThread implements Runnable
|
||||
switch (w.getType()){
|
||||
case BOSS:
|
||||
BossWave bw = (BossWave) w;
|
||||
double maxHealth = bw.getMaxHealth(playerCount);
|
||||
double maxHealth = bw.getHealth().evaluate(arena);
|
||||
MABoss boss = monsterManager.addBoss(e, maxHealth);
|
||||
HealthBar healthbar = createsHealthBar.create(e, bw.getBossName());
|
||||
arena.getPlayersInArena().forEach(healthbar::addPlayer);
|
||||
|
@ -2,6 +2,8 @@ package com.garbagemule.MobArena;
|
||||
|
||||
import com.garbagemule.MobArena.commands.CommandHandler;
|
||||
import com.garbagemule.MobArena.config.LoadsConfigFile;
|
||||
import com.garbagemule.MobArena.formula.FormulaMacros;
|
||||
import com.garbagemule.MobArena.formula.FormulaManager;
|
||||
import com.garbagemule.MobArena.framework.Arena;
|
||||
import com.garbagemule.MobArena.framework.ArenaMaster;
|
||||
import com.garbagemule.MobArena.listeners.MAGlobalListener;
|
||||
@ -60,6 +62,8 @@ public class MobArena extends JavaPlugin
|
||||
private Messenger messenger;
|
||||
private ThingManager thingman;
|
||||
private ThingPickerManager pickman;
|
||||
private FormulaManager formman;
|
||||
private FormulaMacros macros;
|
||||
|
||||
private SignListeners signListeners;
|
||||
|
||||
@ -71,6 +75,8 @@ public class MobArena extends JavaPlugin
|
||||
pickman.register(new ThingGroupPickerParser(pickman));
|
||||
pickman.register(new RandomThingPickerParser(pickman, random));
|
||||
pickman.register(new NothingPickerParser());
|
||||
|
||||
formman = FormulaManager.createDefault();
|
||||
}
|
||||
|
||||
public void onEnable() {
|
||||
@ -97,6 +103,7 @@ public class MobArena extends JavaPlugin
|
||||
private void setup() {
|
||||
try {
|
||||
createDataFolder();
|
||||
setupFormulaMacros();
|
||||
setupArenaMaster();
|
||||
setupCommandHandler();
|
||||
|
||||
@ -121,6 +128,10 @@ public class MobArena extends JavaPlugin
|
||||
}
|
||||
}
|
||||
|
||||
private void setupFormulaMacros() {
|
||||
macros = FormulaMacros.create(this);
|
||||
}
|
||||
|
||||
private void setupArenaMaster() {
|
||||
arenaMaster = new ArenaMasterImpl(this);
|
||||
}
|
||||
@ -173,6 +184,7 @@ public class MobArena extends JavaPlugin
|
||||
try {
|
||||
reloadConfig();
|
||||
reloadGlobalMessenger();
|
||||
reloadFormulaMacros();
|
||||
reloadArenaMaster();
|
||||
reloadAnnouncementsFile();
|
||||
reloadSigns();
|
||||
@ -198,6 +210,14 @@ public class MobArena extends JavaPlugin
|
||||
messenger = new Messenger(prefix);
|
||||
}
|
||||
|
||||
private void reloadFormulaMacros() {
|
||||
try {
|
||||
macros.reload();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("There was an error reloading the formulas-file:\n" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadArenaMaster() {
|
||||
arenaMaster.getArenas().forEach(Arena::forceEnd);
|
||||
arenaMaster.initialize();
|
||||
@ -281,4 +301,12 @@ public class MobArena extends JavaPlugin
|
||||
public ThingPickerManager getThingPickerManager() {
|
||||
return pickman;
|
||||
}
|
||||
|
||||
public FormulaManager getFormulaManager() {
|
||||
return formman;
|
||||
}
|
||||
|
||||
public FormulaMacros getFormulaMacros() {
|
||||
return macros;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
class ArgumentMismatch extends FormulaError {
|
||||
|
||||
private static final String few = "Not enough arguments for %s(%s), expected %d, got %d";
|
||||
private static final String many = "Too many arguments for %s(%s), expected %d, got %d";
|
||||
|
||||
ArgumentMismatch(Lexeme lexeme, int expected, int counted, String input) {
|
||||
super(message(lexeme, expected, counted), input, lexeme.pos);
|
||||
}
|
||||
|
||||
private static String message(Lexeme lexeme, int expected, int counted) {
|
||||
String name = lexeme.value;
|
||||
String args = args(expected);
|
||||
String template = (counted < expected) ? few : many;
|
||||
return String.format(template, name, args, expected, counted);
|
||||
}
|
||||
|
||||
private static String args(int count) {
|
||||
if (count == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
char current = 'a';
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append(current);
|
||||
|
||||
for (int i = 1; i < count; i++) {
|
||||
current++;
|
||||
result.append(",").append(current);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import com.garbagemule.MobArena.framework.Arena;
|
||||
|
||||
class BinaryFormula implements Formula {
|
||||
|
||||
private final BinaryOperation operation;
|
||||
private final Formula left;
|
||||
private final Formula right;
|
||||
|
||||
BinaryFormula(BinaryOperation operation, Formula left, Formula right) {
|
||||
this.operation = operation;
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double evaluate(Arena arena) {
|
||||
double a = left.evaluate(arena);
|
||||
double b = right.evaluate(arena);
|
||||
return operation.apply(a, b);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
class BinaryFunction {
|
||||
|
||||
final String name;
|
||||
final BinaryOperation operation;
|
||||
|
||||
BinaryFunction(String name, BinaryOperation operation) {
|
||||
this.name = name;
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
Formula create(Formula left, Formula right) {
|
||||
return new BinaryFormula(operation, left, right);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface BinaryOperation extends BiFunction<Double, Double, Double> {
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
class BinaryOperator {
|
||||
|
||||
final String symbol;
|
||||
final int precedence;
|
||||
final boolean left;
|
||||
final BinaryOperation operation;
|
||||
|
||||
BinaryOperator(String symbol, int precedence, boolean left, BinaryOperation operation) {
|
||||
this.symbol = symbol;
|
||||
this.precedence = precedence;
|
||||
this.left = left;
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
Formula create(Formula left, Formula right) {
|
||||
return new BinaryFormula(operation, left, right);
|
||||
}
|
||||
|
||||
}
|
170
src/main/java/com/garbagemule/MobArena/formula/Environment.java
Normal file
170
src/main/java/com/garbagemule/MobArena/formula/Environment.java
Normal file
@ -0,0 +1,170 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
class Environment {
|
||||
|
||||
final List<Token> unary;
|
||||
final List<Token> binary;
|
||||
final List<Token> symbols;
|
||||
|
||||
final Map<String, Double> constants;
|
||||
final Map<String, Formula> variables;
|
||||
final Map<String, UnaryOperator> unaryOperators;
|
||||
final Map<String, BinaryOperator> binaryOperators;
|
||||
final Map<String, UnaryFunction> unaryFunctions;
|
||||
final Map<String, BinaryFunction> binaryFunctions;
|
||||
|
||||
private Environment() {
|
||||
unary = new ArrayList<>();
|
||||
binary = new ArrayList<>();
|
||||
symbols = Arrays.asList(
|
||||
Token.LEFT_PAREN,
|
||||
Token.RIGHT_PAREN,
|
||||
Token.COMMA
|
||||
);
|
||||
|
||||
constants = new HashMap<>();
|
||||
variables = new HashMap<>();
|
||||
unaryOperators = new HashMap<>();
|
||||
binaryOperators = new HashMap<>();
|
||||
unaryFunctions = new HashMap<>();
|
||||
binaryFunctions = new HashMap<>();
|
||||
}
|
||||
|
||||
void registerConstant(String name, double value) {
|
||||
constants.put(name, value);
|
||||
}
|
||||
|
||||
void registerVariable(String name, Formula formula) {
|
||||
variables.put(name, formula);
|
||||
}
|
||||
|
||||
void registerUnaryOperator(String symbol, int precedence, UnaryOperation operation) {
|
||||
unaryOperators.put(symbol, new UnaryOperator(symbol, precedence, operation));
|
||||
registerOperatorToken(TokenType.UNARY_OPERATOR, symbol, unary);
|
||||
}
|
||||
|
||||
void registerBinaryOperator(String symbol, int precedence, boolean left, BinaryOperation operation) {
|
||||
binaryOperators.put(symbol, new BinaryOperator(symbol, precedence, left, operation));
|
||||
registerOperatorToken(TokenType.BINARY_OPERATOR, symbol, binary);
|
||||
}
|
||||
|
||||
void registerUnaryFunction(String name, UnaryOperation operation) {
|
||||
unaryFunctions.put(name, new UnaryFunction(name, operation));
|
||||
}
|
||||
|
||||
void registerBinaryFunction(String name, BinaryOperation operation) {
|
||||
binaryFunctions.put(name, new BinaryFunction(name, operation));
|
||||
}
|
||||
|
||||
boolean isConstant(String identifier) {
|
||||
return constants.containsKey(identifier);
|
||||
}
|
||||
|
||||
boolean isVariable(String identifier) {
|
||||
return variables.containsKey(identifier);
|
||||
}
|
||||
|
||||
boolean isUnaryOperator(String symbol) {
|
||||
return unaryOperators.containsKey(symbol);
|
||||
}
|
||||
|
||||
boolean isBinaryOperator(String symbol) {
|
||||
return binaryOperators.containsKey(symbol);
|
||||
}
|
||||
|
||||
boolean isUnaryFunction(String identifier) {
|
||||
return unaryFunctions.containsKey(identifier);
|
||||
}
|
||||
|
||||
boolean isBinaryFunction(String identifier) {
|
||||
return binaryFunctions.containsKey(identifier);
|
||||
}
|
||||
|
||||
boolean isFunction(String identifier) {
|
||||
return isUnaryFunction(identifier)
|
||||
|| isBinaryFunction(identifier);
|
||||
}
|
||||
|
||||
double getConstant(String identifier) {
|
||||
return constants.get(identifier);
|
||||
}
|
||||
|
||||
Formula getVariable(String identifier) {
|
||||
return variables.get(identifier);
|
||||
}
|
||||
|
||||
UnaryOperator getUnaryOperator(String symbol) {
|
||||
return unaryOperators.get(symbol);
|
||||
}
|
||||
|
||||
BinaryOperator getBinaryOperator(String symbol) {
|
||||
return binaryOperators.get(symbol);
|
||||
}
|
||||
|
||||
UnaryFunction getUnaryFunction(String identifier) {
|
||||
return unaryFunctions.get(identifier);
|
||||
}
|
||||
|
||||
BinaryFunction getBinaryFunction(String identifier) {
|
||||
return binaryFunctions.get(identifier);
|
||||
}
|
||||
|
||||
private void registerOperatorToken(TokenType type, String operator, List<Token> operators) {
|
||||
int i;
|
||||
for (i = 0; i < operators.size(); i++) {
|
||||
if (operators.get(i).symbol.length() < operator.length()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
String escaped = operator.replaceAll("", "\\\\");
|
||||
String trimmed = escaped.substring(0, escaped.length() - 1);
|
||||
Token token = new Token(type, trimmed, operator);
|
||||
operators.add(i, token);
|
||||
}
|
||||
|
||||
@SuppressWarnings("Convert2MethodRef")
|
||||
static Environment createDefault() {
|
||||
Environment result = new Environment();
|
||||
|
||||
// Constants
|
||||
result.registerConstant("pi", Math.PI);
|
||||
result.registerConstant("e", Math.E);
|
||||
|
||||
// Unary operators
|
||||
result.registerUnaryOperator("+", 4, value -> +value);
|
||||
result.registerUnaryOperator("-", 4, value -> -value);
|
||||
|
||||
// Binary operators
|
||||
result.registerBinaryOperator("+", 2, true, (a, b) -> a + b);
|
||||
result.registerBinaryOperator("-", 2, true, (a, b) -> a - b);
|
||||
result.registerBinaryOperator("*", 3, true, (a, b) -> a * b);
|
||||
result.registerBinaryOperator("/", 3, true, (a, b) -> a / b);
|
||||
result.registerBinaryOperator("%", 3, true, (a, b) -> a % b);
|
||||
result.registerBinaryOperator("^", 4, false, (a, b) -> Math.pow(a, b));
|
||||
|
||||
// Unary functions
|
||||
result.registerUnaryFunction("sqrt", Math::sqrt);
|
||||
result.registerUnaryFunction("abs", Math::abs);
|
||||
|
||||
result.registerUnaryFunction("ceil", Math::ceil);
|
||||
result.registerUnaryFunction("floor", Math::floor);
|
||||
result.registerUnaryFunction("round", value -> (double) Math.round(value));
|
||||
|
||||
result.registerUnaryFunction("sin", Math::sin);
|
||||
result.registerUnaryFunction("cos", Math::cos);
|
||||
result.registerUnaryFunction("tan", Math::tan);
|
||||
|
||||
// Binary functions
|
||||
result.registerBinaryFunction("min", Math::min);
|
||||
result.registerBinaryFunction("max", Math::max);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import com.garbagemule.MobArena.framework.Arena;
|
||||
|
||||
public interface Formula {
|
||||
|
||||
double evaluate(Arena arena);
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
class FormulaError extends IllegalArgumentException {
|
||||
|
||||
private final String input;
|
||||
private final int pos;
|
||||
|
||||
FormulaError(String message, String input, int pos) {
|
||||
super(message);
|
||||
this.input = input;
|
||||
this.pos = pos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
String arrow = arrow(pos + 1);
|
||||
String template = "%s\n%s\n%s";
|
||||
return String.format(template, super.getMessage(), input, arrow);
|
||||
}
|
||||
|
||||
private String arrow(int length) {
|
||||
char[] value = new char[length];
|
||||
Arrays.fill(value, ' ');
|
||||
value[length - 1] = '^';
|
||||
return new String(value);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import com.garbagemule.MobArena.MobArena;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class FormulaMacros {
|
||||
|
||||
private static final String FILENAME = "formulas.yml";
|
||||
|
||||
private final Path file;
|
||||
private final Map<String, Map<String, String>> macros;
|
||||
|
||||
private FormulaMacros(Path file) {
|
||||
this.file = file;
|
||||
this.macros = new HashMap<>();
|
||||
}
|
||||
|
||||
public void reload() throws IOException {
|
||||
byte[] bytes = Files.readAllBytes(file);
|
||||
String content = new String(bytes);
|
||||
|
||||
Yaml yaml = new Yaml();
|
||||
Map<?, ?> raw = yaml.load(content);
|
||||
Map<String, Map<String, String>> converted = convert(raw);
|
||||
|
||||
macros.clear();
|
||||
macros.putAll(converted);
|
||||
}
|
||||
|
||||
private Map<String, Map<String, String>> convert(Map<?, ?> raw) {
|
||||
Map<String, Map<String, String>> result = new HashMap<>();
|
||||
|
||||
for (Map.Entry<?, ?> entry : raw.entrySet()) {
|
||||
String section = String.valueOf(entry.getKey());
|
||||
Map<String, String> macros = convert(entry.getValue());
|
||||
if (macros != null) {
|
||||
result.put(section, macros);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, String> convert(Object raw) {
|
||||
if (raw instanceof Map) {
|
||||
Map<String, String> macros = new HashMap<>();
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) raw).entrySet()) {
|
||||
String macro = String.valueOf(entry.getKey());
|
||||
String formula = String.valueOf(entry.getValue());
|
||||
macros.put(macro, formula);
|
||||
}
|
||||
return macros;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String get(String key) {
|
||||
return get(null, key);
|
||||
}
|
||||
|
||||
public String get(String section, String key) {
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
if (section != null) {
|
||||
String macro = lookup(section, key);
|
||||
if (macro != null) {
|
||||
return macro;
|
||||
}
|
||||
}
|
||||
return lookup("global", key);
|
||||
}
|
||||
|
||||
private String lookup(String section, String key) {
|
||||
Map<String, String> specific = macros.get(section);
|
||||
if (specific == null) {
|
||||
return null;
|
||||
}
|
||||
return specific.get(key);
|
||||
}
|
||||
|
||||
public static FormulaMacros create(MobArena plugin) {
|
||||
File file = new File(plugin.getDataFolder(), FILENAME);
|
||||
if (!file.exists()) {
|
||||
plugin.getLogger().info(FILENAME + " not found, creating default...");
|
||||
plugin.saveResource(FILENAME, false);
|
||||
}
|
||||
return new FormulaMacros(file.toPath());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import com.garbagemule.MobArena.framework.Arena;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FormulaManager {
|
||||
|
||||
private final Environment env;
|
||||
private final Lexer lexer;
|
||||
private final Parser parser;
|
||||
|
||||
FormulaManager(
|
||||
Environment env,
|
||||
Lexer lexer,
|
||||
Parser parser
|
||||
) {
|
||||
this.env = env;
|
||||
this.lexer = lexer;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void registerConstant(String name, double value) {
|
||||
env.registerConstant(name, value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void registerVariable(String name, Formula formula) {
|
||||
env.registerVariable(name, formula);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void registerUnaryOperator(String symbol, int precedence, UnaryOperation operation) {
|
||||
env.registerUnaryOperator(symbol, precedence, operation);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void registerBinaryOperator(String symbol, int precedence, boolean left, BinaryOperation operation) {
|
||||
env.registerBinaryOperator(symbol, precedence, left, operation);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void registerUnaryFunction(String name, UnaryOperation operation) {
|
||||
env.registerUnaryFunction(name, operation);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void registerBinaryFunction(String name, BinaryOperation operation) {
|
||||
env.registerBinaryFunction(name, operation);
|
||||
}
|
||||
|
||||
public Formula parse(String input) {
|
||||
List<Lexeme> infix = lexer.tokenize(input);
|
||||
return parser.parse(input, infix);
|
||||
}
|
||||
|
||||
public static FormulaManager createDefault() {
|
||||
Environment env = Environment.createDefault();
|
||||
|
||||
// Wave number variables
|
||||
env.registerVariable("current-wave", a -> a.getWaveManager().getWaveNumber());
|
||||
env.registerVariable("final-wave", a -> a.getWaveManager().getFinalWave());
|
||||
|
||||
// Player count variables
|
||||
env.registerVariable("initial-players", Arena::getPlayerCount);
|
||||
env.registerVariable("live-players", a -> a.getPlayersInArena().size());
|
||||
env.registerVariable("dead-players", a -> a.getPlayerCount() - a.getPlayersInArena().size());
|
||||
env.registerVariable("min-players", Arena::getMinPlayers);
|
||||
env.registerVariable("max-players", Arena::getMaxPlayers);
|
||||
|
||||
// Monster count variables
|
||||
env.registerVariable("live-monsters", a -> a.getMonsterManager().getMonsters().size());
|
||||
|
||||
Lexer lexer = new Lexer(env);
|
||||
Parser parser = new Parser(env);
|
||||
|
||||
return new FormulaManager(env, lexer, parser);
|
||||
}
|
||||
|
||||
}
|
37
src/main/java/com/garbagemule/MobArena/formula/Formulas.java
Normal file
37
src/main/java/com/garbagemule/MobArena/formula/Formulas.java
Normal file
@ -0,0 +1,37 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
public class Formulas {
|
||||
|
||||
/**
|
||||
* Default "old" style wave growth. Equivalent to the formula:
|
||||
* <pre>{@code <initial-players> + <current-wave>}</pre>
|
||||
*/
|
||||
public static final Formula DEFAULT_WAVE_GROWTH = (arena) -> {
|
||||
int players = arena.getPlayerCount();
|
||||
int wave = arena.getWaveManager().getWaveNumber();
|
||||
return players + wave;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default "low" swarm amount. Equivalent to the formula:
|
||||
* <pre>{@code max(1, <initial-players> / 2) * 10}</pre>
|
||||
*/
|
||||
public static final Formula DEFAULT_SWARM_AMOUNT = (arena) -> {
|
||||
int players = arena.getPlayerCount();
|
||||
return Math.max(1, players / 2) * 10;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default "medium" boss health. Equivalent to the formula:
|
||||
* <pre>{@code (<initial-players> + 1) 20 * 15}</pre>
|
||||
*/
|
||||
public static final Formula DEFAULT_BOSS_HEALTH = (arena) -> {
|
||||
int players = arena.getPlayerCount();
|
||||
return (players + 1) * 20 * 8;
|
||||
};
|
||||
|
||||
private Formulas() {
|
||||
// OK BOSS
|
||||
}
|
||||
|
||||
}
|
20
src/main/java/com/garbagemule/MobArena/formula/Lexeme.java
Normal file
20
src/main/java/com/garbagemule/MobArena/formula/Lexeme.java
Normal file
@ -0,0 +1,20 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
class Lexeme {
|
||||
|
||||
final Token token;
|
||||
final String value;
|
||||
final int pos;
|
||||
|
||||
Lexeme(Token token, String value, int pos) {
|
||||
this.token = token;
|
||||
this.value = value;
|
||||
this.pos = pos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return token.type + " '" + value + "'";
|
||||
}
|
||||
|
||||
}
|
213
src/main/java/com/garbagemule/MobArena/formula/Lexer.java
Normal file
213
src/main/java/com/garbagemule/MobArena/formula/Lexer.java
Normal file
@ -0,0 +1,213 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class Lexer {
|
||||
|
||||
private final Environment env;
|
||||
|
||||
private List<Lexeme> result;
|
||||
private String input;
|
||||
private int pos;
|
||||
|
||||
Lexer(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
List<Lexeme> tokenize(String input) {
|
||||
this.result = new ArrayList<>();
|
||||
this.input = input;
|
||||
this.pos = 0;
|
||||
|
||||
tokenize();
|
||||
|
||||
List<Lexeme> result = this.result;
|
||||
this.result = null;
|
||||
this.input = null;
|
||||
this.pos = -1;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void tokenize() {
|
||||
while (pos < input.length()) {
|
||||
skipWhitespace();
|
||||
nextToken();
|
||||
}
|
||||
}
|
||||
|
||||
private void skipWhitespace() {
|
||||
for (int i = pos; i < input.length(); i++) {
|
||||
char current = input.charAt(i);
|
||||
if (Character.isWhitespace(current)) {
|
||||
continue;
|
||||
}
|
||||
pos = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void nextToken() {
|
||||
if (nextNumber()) {
|
||||
return;
|
||||
}
|
||||
if (nextIdentifier()) {
|
||||
return;
|
||||
}
|
||||
if (nextVariable()) {
|
||||
return;
|
||||
}
|
||||
if (nextOperator()) {
|
||||
return;
|
||||
}
|
||||
if (nextSymbol()) {
|
||||
return;
|
||||
}
|
||||
String message = String.format("Unexpected token in column %d", pos + 1);
|
||||
throw new LexerError(message, input, pos);
|
||||
}
|
||||
|
||||
private boolean nextNumber() {
|
||||
char first = input.charAt(pos);
|
||||
if (!Character.isDigit(first)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String chunk = input.substring(pos);
|
||||
int end = Token.NUMBER.match(chunk);
|
||||
if (end < 0) {
|
||||
String message = String.format("Invalid number in column %d", pos + 1);
|
||||
throw new LexerError(message, input, pos);
|
||||
}
|
||||
|
||||
String lexeme = input.substring(pos, pos + end);
|
||||
result.add(new Lexeme(Token.NUMBER, lexeme, pos));
|
||||
pos += end;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean nextIdentifier() {
|
||||
char first = input.charAt(pos);
|
||||
if (!Character.isLetter(first)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String chunk = input.substring(pos);
|
||||
int end = Token.IDENTIFIER.match(chunk);
|
||||
if (end < 0) {
|
||||
String message = String.format("Invalid identifier in column %d", pos + 1);
|
||||
throw new LexerError(message, input, pos);
|
||||
}
|
||||
|
||||
if (pos + end < input.length()) {
|
||||
if (input.charAt(pos + end) == '>') {
|
||||
String message = String.format("Unmatched right bracket in column %d", pos + 1);
|
||||
throw new LexerError(message, input, pos);
|
||||
}
|
||||
}
|
||||
|
||||
String identifier = input.substring(pos, pos + end);
|
||||
if (!env.isConstant(identifier) && !env.isFunction(identifier)) {
|
||||
throw new UnknownToken("identifier", identifier, input, pos);
|
||||
}
|
||||
|
||||
result.add(new Lexeme(Token.IDENTIFIER, identifier, pos));
|
||||
pos += end;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean nextVariable() {
|
||||
char first = input.charAt(pos);
|
||||
if (first == '>') {
|
||||
String message = String.format("Unmatched right bracket in column %d", pos + 1);
|
||||
throw new LexerError(message, input, pos);
|
||||
}
|
||||
if (first != '<') {
|
||||
return false;
|
||||
}
|
||||
|
||||
String chunk = input.substring(pos);
|
||||
int end = Token.VARIABLE.match(chunk);
|
||||
if (end < 0) {
|
||||
String message = String.format("Invalid variable in column %d", pos + 1);
|
||||
throw new LexerError(message, input, pos);
|
||||
}
|
||||
if (end >= chunk.length() || chunk.charAt(end) != '>') {
|
||||
String message = String.format("Unmatched left bracket in column %d", pos + 1);
|
||||
throw new LexerError(message, input, pos);
|
||||
}
|
||||
|
||||
String identifier = input.substring(pos + 1, pos + end);
|
||||
if (!env.isVariable(identifier)) {
|
||||
String varible = "<" + identifier + ">";
|
||||
throw new UnknownToken("variable", varible, input, pos);
|
||||
}
|
||||
|
||||
result.add(new Lexeme(Token.VARIABLE, identifier, pos));
|
||||
pos += end + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean nextOperator() {
|
||||
if (isUnaryTokenExpected()) {
|
||||
return nextUnaryOperator();
|
||||
} else {
|
||||
return nextBinaryOperator();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean nextUnaryOperator() {
|
||||
return nextToken(env.unary);
|
||||
}
|
||||
|
||||
private boolean nextBinaryOperator() {
|
||||
return nextToken(env.binary);
|
||||
}
|
||||
|
||||
private boolean nextSymbol() {
|
||||
return nextToken(env.symbols);
|
||||
}
|
||||
|
||||
private boolean nextToken(List<Token> tokens) {
|
||||
String chunk = input.substring(pos);
|
||||
for (Token token : tokens) {
|
||||
int end = token.match(chunk);
|
||||
if (end < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String symbol = input.substring(pos, pos + end);
|
||||
result.add(new Lexeme(token, symbol, pos));
|
||||
pos += end;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isUnaryTokenExpected() {
|
||||
if (result.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Lexeme previous = result.get(result.size() - 1);
|
||||
|
||||
switch (previous.token.type) {
|
||||
case LEFT_PAREN:
|
||||
case UNARY_OPERATOR:
|
||||
case BINARY_OPERATOR:
|
||||
case COMMA: {
|
||||
return true;
|
||||
}
|
||||
case NUMBER:
|
||||
case IDENTIFIER:
|
||||
case VARIABLE:
|
||||
case RIGHT_PAREN: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnknownToken("symbol", previous, input);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
class LexerError extends FormulaError {
|
||||
|
||||
LexerError(String message, String input, int pos) {
|
||||
super(message, input, pos);
|
||||
}
|
||||
|
||||
}
|
338
src/main/java/com/garbagemule/MobArena/formula/Parser.java
Normal file
338
src/main/java/com/garbagemule/MobArena/formula/Parser.java
Normal file
@ -0,0 +1,338 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
|
||||
class Parser {
|
||||
|
||||
private final Environment env;
|
||||
|
||||
private Deque<Formula> output;
|
||||
private Deque<Lexeme> stack;
|
||||
private Deque<Integer> args;
|
||||
private String source;
|
||||
private List<Lexeme> input;
|
||||
|
||||
Parser(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
Formula parse(String source, List<Lexeme> input) {
|
||||
this.output = new ArrayDeque<>();
|
||||
this.stack = new ArrayDeque<>();
|
||||
this.args = new ArrayDeque<>();
|
||||
this.source = source;
|
||||
this.input = input;
|
||||
|
||||
Formula result = parse();
|
||||
this.output = null;
|
||||
this.stack = null;
|
||||
this.args = null;
|
||||
this.source = null;
|
||||
this.input = null;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Formula parse() {
|
||||
for (Lexeme lexeme : input) {
|
||||
switch (lexeme.token.type) {
|
||||
case NUMBER: {
|
||||
number(lexeme);
|
||||
break;
|
||||
}
|
||||
case IDENTIFIER: {
|
||||
identifier(lexeme);
|
||||
break;
|
||||
}
|
||||
case VARIABLE: {
|
||||
variable(lexeme);
|
||||
break;
|
||||
}
|
||||
case UNARY_OPERATOR: {
|
||||
unary(lexeme);
|
||||
break;
|
||||
}
|
||||
case BINARY_OPERATOR: {
|
||||
binary(lexeme);
|
||||
break;
|
||||
}
|
||||
case LEFT_PAREN: {
|
||||
left(lexeme);
|
||||
break;
|
||||
}
|
||||
case RIGHT_PAREN: {
|
||||
right(lexeme);
|
||||
break;
|
||||
}
|
||||
case COMMA: {
|
||||
comma(lexeme);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new UnexpectedToken(lexeme, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Once we get to the end of the infix expression, we'll need to pop
|
||||
// off any remaining operators on the stack. Anything other than an
|
||||
// operator token is an error, since parenthesized expressions would
|
||||
// have already been completely resolved during conversion.
|
||||
while (!stack.isEmpty()) {
|
||||
Lexeme top = stack.peek();
|
||||
if (popIfOperator(top)) {
|
||||
continue;
|
||||
}
|
||||
if (top.token.type == TokenType.LEFT_PAREN) {
|
||||
throw new UnmatchedParenthesis(top, source);
|
||||
}
|
||||
throw new UnexpectedToken(top, source);
|
||||
}
|
||||
|
||||
if (output.size() != 1) {
|
||||
throw new IllegalArgumentException("wtf bitchhhh");
|
||||
}
|
||||
|
||||
return output.pop();
|
||||
}
|
||||
|
||||
private void number(Lexeme lexeme) {
|
||||
// Number tokens go straight to the output.
|
||||
double value = Double.parseDouble(lexeme.value);
|
||||
Formula formula = new ValueFormula(value);
|
||||
output.push(formula);
|
||||
}
|
||||
|
||||
private void identifier(Lexeme lexeme) {
|
||||
// Identifiers are either constants or functions.
|
||||
String identifier = lexeme.value;
|
||||
|
||||
if (env.isConstant(identifier)) {
|
||||
// Constants go straight to the output.
|
||||
double value = env.getConstant(identifier);
|
||||
Formula formula = new ValueFormula(value);
|
||||
output.push(formula);
|
||||
return;
|
||||
}
|
||||
|
||||
if (env.isFunction(identifier)) {
|
||||
// Functions go on the stack.
|
||||
stack.push(lexeme);
|
||||
args.push(1);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UnknownToken("identifier", lexeme, source);
|
||||
}
|
||||
|
||||
private void variable(Lexeme lexeme) {
|
||||
// Variables go straight to the output.
|
||||
String identifier = lexeme.value;
|
||||
|
||||
if (env.isVariable(identifier)) {
|
||||
Formula formula = env.getVariable(identifier);
|
||||
output.push(formula);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UnknownToken("variable", lexeme, source);
|
||||
}
|
||||
|
||||
private void unary(Lexeme lexeme) {
|
||||
String symbol = lexeme.value;
|
||||
|
||||
if (!env.isUnaryOperator(symbol)) {
|
||||
throw new UnknownToken("unary operator", lexeme, source);
|
||||
}
|
||||
|
||||
stack.push(lexeme);
|
||||
}
|
||||
|
||||
private void binary(Lexeme lexeme) {
|
||||
String symbol = lexeme.value;
|
||||
|
||||
if (!env.isBinaryOperator(symbol)) {
|
||||
throw new UnknownToken("binary operator", lexeme, source);
|
||||
}
|
||||
|
||||
BinaryOperator current = env.getBinaryOperator(symbol);
|
||||
|
||||
while (!stack.isEmpty()) {
|
||||
Lexeme peek = stack.peek();
|
||||
String top = peek.value;
|
||||
|
||||
if (peek.token.type == TokenType.UNARY_OPERATOR) {
|
||||
if (!env.isUnaryOperator(top)) {
|
||||
throw new UnknownToken("unary operator", peek, source);
|
||||
}
|
||||
|
||||
// It would be abuse of semantics to call unary operators
|
||||
// "associative", but if we treat them as though they are
|
||||
// right-associative, we can get robust behavior that will
|
||||
// result in -2^4 evaluating to -16 (rather than 16) if we
|
||||
// assign unary minus and exponentiation the same operator
|
||||
// precedence.
|
||||
UnaryOperator candidate = env.getUnaryOperator(top);
|
||||
if (candidate.precedence > current.precedence) {
|
||||
popIfOperator(peek);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (peek.token.type == TokenType.BINARY_OPERATOR) {
|
||||
if (!env.isBinaryOperator(top)) {
|
||||
throw new UnknownToken("binary operator", peek, source);
|
||||
}
|
||||
|
||||
// Other binary operators get popped if they have a higher
|
||||
// precedence, or if they have equal precedence and are
|
||||
// left-associative.
|
||||
BinaryOperator candidate = env.getBinaryOperator(top);
|
||||
if (candidate.precedence > current.precedence) {
|
||||
popIfOperator(peek);
|
||||
continue;
|
||||
}
|
||||
if (candidate.precedence == current.precedence && current.left) {
|
||||
popIfOperator(peek);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// A left parenthesis means we need to resolve before a
|
||||
// new expression can begin. A right parenthesis means
|
||||
// we need to resolve before the current expression ends.
|
||||
// Either way, we don't pop anything.
|
||||
if (peek.token.type == TokenType.LEFT_PAREN || peek.token.type == TokenType.RIGHT_PAREN) {
|
||||
break;
|
||||
}
|
||||
|
||||
throw new UnexpectedToken(peek, source);
|
||||
}
|
||||
|
||||
stack.push(lexeme);
|
||||
}
|
||||
|
||||
private void left(Lexeme lexeme) {
|
||||
// Left parentheses go on the stack.
|
||||
stack.push(lexeme);
|
||||
}
|
||||
|
||||
private void right(Lexeme lexeme) {
|
||||
if (stack.isEmpty()) {
|
||||
throw new UnmatchedParenthesis(lexeme, source);
|
||||
}
|
||||
|
||||
// Right parentheses act as terminators in much the same way
|
||||
// infix operators with low precedence do.
|
||||
while (!stack.isEmpty()) {
|
||||
Lexeme peek = stack.peek();
|
||||
|
||||
// Operators go straight to the output.
|
||||
if (popIfOperator(peek)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (peek.token.type == TokenType.LEFT_PAREN) {
|
||||
// When we hit a left parenthesis, the expression that
|
||||
// the current right parenthesis belongs to is resolved,
|
||||
// so it just needs to get popped. Then, if the top of
|
||||
// the stack is a function token, it is also popped.
|
||||
stack.pop();
|
||||
|
||||
if (!stack.isEmpty()) {
|
||||
popIfFunction(stack.peek());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
throw new UnexpectedToken(peek, source);
|
||||
}
|
||||
}
|
||||
|
||||
private void comma(Lexeme lexeme) {
|
||||
while (!stack.isEmpty()) {
|
||||
Lexeme top = stack.peek();
|
||||
TokenType type = top.token.type;
|
||||
|
||||
if (popIfOperator(top)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == TokenType.LEFT_PAREN) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (type == TokenType.RIGHT_PAREN) {
|
||||
throw new UnexpectedToken(top, source);
|
||||
}
|
||||
|
||||
throw new UnexpectedToken(lexeme, source);
|
||||
}
|
||||
|
||||
args.push(args.pop() + 1);
|
||||
}
|
||||
|
||||
private boolean popIfOperator(Lexeme lexeme) {
|
||||
String symbol = lexeme.value;
|
||||
|
||||
if (lexeme.token.type == TokenType.UNARY_OPERATOR) {
|
||||
if (env.isUnaryOperator(symbol)) {
|
||||
UnaryOperator operator = env.getUnaryOperator(symbol);
|
||||
Formula argument = output.pop();
|
||||
output.push(operator.create(argument));
|
||||
stack.pop();
|
||||
return true;
|
||||
}
|
||||
throw new UnknownToken("unary operator", lexeme, source);
|
||||
}
|
||||
|
||||
if (lexeme.token.type == TokenType.BINARY_OPERATOR) {
|
||||
if (env.isBinaryOperator(symbol)) {
|
||||
BinaryOperator operator = env.getBinaryOperator(symbol);
|
||||
Formula right = output.pop();
|
||||
Formula left = output.pop();
|
||||
output.push(operator.create(left, right));
|
||||
stack.pop();
|
||||
return true;
|
||||
}
|
||||
throw new UnknownToken("binary operator", lexeme, source);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void popIfFunction(Lexeme lexeme) {
|
||||
if (lexeme.token.type != TokenType.IDENTIFIER) {
|
||||
return;
|
||||
}
|
||||
|
||||
String identifier = lexeme.value;
|
||||
|
||||
if (env.isUnaryFunction(identifier)) {
|
||||
UnaryFunction function = env.getUnaryFunction(identifier);
|
||||
Integer counted = args.pop();
|
||||
if (counted != 1) {
|
||||
throw new ArgumentMismatch(lexeme, 1, counted, source);
|
||||
}
|
||||
Formula argument = output.pop();
|
||||
output.push(function.create(argument));
|
||||
stack.pop();
|
||||
} else if (env.isBinaryFunction(identifier)) {
|
||||
BinaryFunction function = env.getBinaryFunction(identifier);
|
||||
Integer counted = args.pop();
|
||||
if (counted != 2) {
|
||||
throw new ArgumentMismatch(lexeme, 2, counted, source);
|
||||
}
|
||||
Formula right = output.pop();
|
||||
Formula left = output.pop();
|
||||
output.push(function.create(left, right));
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
class ParserError extends FormulaError {
|
||||
|
||||
ParserError(String message, String input, Lexeme lexeme) {
|
||||
super(message, input, lexeme.pos);
|
||||
}
|
||||
|
||||
}
|
77
src/main/java/com/garbagemule/MobArena/formula/Token.java
Normal file
77
src/main/java/com/garbagemule/MobArena/formula/Token.java
Normal file
@ -0,0 +1,77 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
class Token {
|
||||
|
||||
static final Token LEFT_PAREN = new Token(TokenType.LEFT_PAREN, "\\(", "(");
|
||||
static final Token RIGHT_PAREN = new Token(TokenType.RIGHT_PAREN, "\\)", ")");
|
||||
static final Token COMMA = new Token(TokenType.COMMA, "\\,", ",");
|
||||
|
||||
static final Token NUMBER = new Token(
|
||||
TokenType.NUMBER,
|
||||
"([0-9]+[.])?[0-9]+([eE][-+]?[0-9]+)?"
|
||||
);
|
||||
|
||||
static final Token IDENTIFIER = new Token(
|
||||
TokenType.IDENTIFIER,
|
||||
"\\p{L}([0-9]|\\p{L})*"
|
||||
);
|
||||
|
||||
static final Token VARIABLE = new Token(
|
||||
TokenType.VARIABLE,
|
||||
"[<]\\p{L}(([0-9_-]|\\p{L})*([0-9]|\\p{L})+)?"
|
||||
);
|
||||
|
||||
final TokenType type;
|
||||
final Pattern pattern;
|
||||
final String symbol;
|
||||
|
||||
Token(TokenType type, String regex, String symbol) {
|
||||
this.type = type;
|
||||
this.pattern = Pattern.compile("^" + regex);
|
||||
this.symbol = symbol;
|
||||
}
|
||||
|
||||
Token(TokenType type, String regex) {
|
||||
this(type, regex, null);
|
||||
}
|
||||
|
||||
int match(CharSequence input) {
|
||||
Matcher m = pattern.matcher(input);
|
||||
if (m.find()) {
|
||||
return m.end();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Token token = (Token) o;
|
||||
return type == token.type
|
||||
&& Objects.equals(symbol, token.symbol);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(type, symbol);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (symbol == null) {
|
||||
return type.toString();
|
||||
} else {
|
||||
return type.toString() + " '" + symbol + "'";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
enum TokenType {
|
||||
NUMBER,
|
||||
IDENTIFIER,
|
||||
VARIABLE,
|
||||
UNARY_OPERATOR,
|
||||
BINARY_OPERATOR,
|
||||
LEFT_PAREN,
|
||||
RIGHT_PAREN,
|
||||
COMMA
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import com.garbagemule.MobArena.framework.Arena;
|
||||
|
||||
class UnaryFormula implements Formula {
|
||||
|
||||
private final UnaryOperation operation;
|
||||
private final Formula argument;
|
||||
|
||||
UnaryFormula(UnaryOperation operation, Formula argument) {
|
||||
this.operation = operation;
|
||||
this.argument = argument;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double evaluate(Arena arena) {
|
||||
double value = argument.evaluate(arena);
|
||||
return operation.apply(value);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
class UnaryFunction {
|
||||
|
||||
final String name;
|
||||
final UnaryOperation operation;
|
||||
|
||||
UnaryFunction(String name, UnaryOperation operation) {
|
||||
this.name = name;
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
Formula create(Formula argument) {
|
||||
return new UnaryFormula(operation, argument);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface UnaryOperation extends Function<Double, Double> {
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
class UnaryOperator {
|
||||
|
||||
final String symbol;
|
||||
final int precedence;
|
||||
final UnaryOperation operation;
|
||||
|
||||
UnaryOperator(String symbol, int precedence, UnaryOperation operation) {
|
||||
this.symbol = symbol;
|
||||
this.precedence = precedence;
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
Formula create(Formula argument) {
|
||||
return new UnaryFormula(operation, argument);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
class UnexpectedToken extends FormulaError {
|
||||
|
||||
private static final String template = "Unexpected token '%s' in column %d";
|
||||
|
||||
UnexpectedToken(Lexeme lexeme, String input) {
|
||||
super(message(lexeme), input, lexeme.pos);
|
||||
}
|
||||
|
||||
private static String message(Lexeme lexeme) {
|
||||
return String.format(template, lexeme.value, lexeme.pos + 1);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
class UnknownToken extends FormulaError {
|
||||
|
||||
private static final String template = "Unknown %s '%s' in column %d";
|
||||
|
||||
UnknownToken(String type, Lexeme lexeme, String input) {
|
||||
super(message(type, lexeme), input, lexeme.pos);
|
||||
}
|
||||
|
||||
UnknownToken(String type, String value, String input, int pos) {
|
||||
super(message(type, value, pos), input, pos);
|
||||
}
|
||||
|
||||
private static String message(String type, Lexeme lexeme) {
|
||||
return message(type, lexeme.value, lexeme.pos);
|
||||
}
|
||||
|
||||
private static String message(String type, String identifier, int pos) {
|
||||
return String.format(template, type, identifier, pos + 1);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
class UnmatchedParenthesis extends FormulaError {
|
||||
|
||||
private static final String template = "Unmatched %s parenthesis '%s' in column %d";
|
||||
|
||||
UnmatchedParenthesis(Lexeme lexeme, String input) {
|
||||
super(message(lexeme), input, lexeme.pos);
|
||||
}
|
||||
|
||||
private static String message(Lexeme lexeme) {
|
||||
String side = (lexeme.token.type == TokenType.LEFT_PAREN) ? "left" : "right";
|
||||
return String.format(template, side, lexeme.value, lexeme.pos + 1);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import com.garbagemule.MobArena.framework.Arena;
|
||||
|
||||
class ValueFormula implements Formula {
|
||||
|
||||
private final double value;
|
||||
|
||||
ValueFormula(double value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double evaluate(Arena arena) {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package com.garbagemule.MobArena.waves;
|
||||
|
||||
import com.garbagemule.MobArena.ConfigError;
|
||||
import com.garbagemule.MobArena.formula.Formula;
|
||||
import com.garbagemule.MobArena.framework.Arena;
|
||||
import com.garbagemule.MobArena.region.ArenaRegion;
|
||||
import com.garbagemule.MobArena.things.InvalidThingInputString;
|
||||
@ -12,10 +13,7 @@ import com.garbagemule.MobArena.util.PotionEffectParser;
|
||||
import com.garbagemule.MobArena.util.Slugs;
|
||||
import com.garbagemule.MobArena.waves.ability.Ability;
|
||||
import com.garbagemule.MobArena.waves.ability.AbilityManager;
|
||||
import com.garbagemule.MobArena.waves.enums.BossHealth;
|
||||
import com.garbagemule.MobArena.waves.enums.SwarmAmount;
|
||||
import com.garbagemule.MobArena.waves.enums.WaveBranch;
|
||||
import com.garbagemule.MobArena.waves.enums.WaveGrowth;
|
||||
import com.garbagemule.MobArena.waves.enums.WaveType;
|
||||
import com.garbagemule.MobArena.waves.types.BossWave;
|
||||
import com.garbagemule.MobArena.waves.types.DefaultWave;
|
||||
@ -171,16 +169,21 @@ public class WaveParser
|
||||
}
|
||||
|
||||
// Grab the WaveGrowth
|
||||
String grw = config.getString("growth", null);
|
||||
if (grw != null && !grw.isEmpty()) {
|
||||
try {
|
||||
WaveGrowth growth = WaveGrowth.valueOf(grw.toUpperCase());
|
||||
result.setGrowth(growth);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ConfigError("Failed to parse wave growth for wave " + name + " of arena " + arena.configName() + ": " + grw);
|
||||
}
|
||||
String growth = config.getString("growth", null);
|
||||
if (growth == null || growth.isEmpty()) {
|
||||
growth = "<initial-players> + <current-wave>";
|
||||
} else {
|
||||
result.setGrowth(WaveGrowth.MEDIUM);
|
||||
String macro = arena.getPlugin().getFormulaMacros().get("wave-growth", growth);
|
||||
if (macro != null) {
|
||||
growth = macro;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Formula formula = arena.getPlugin().getFormulaManager().parse(growth);
|
||||
result.setGrowth(formula);
|
||||
} catch (IllegalArgumentException e) {
|
||||
String message = String.format("Failed to parse wave growth for wave %s of arena %s: %s\n%s", name, arena.configName(), growth, e.getMessage());
|
||||
throw new ConfigError(message);
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -198,16 +201,21 @@ public class WaveParser
|
||||
SwarmWave result = new SwarmWave(monster);
|
||||
|
||||
// Grab SwarmAmount
|
||||
String amnt = config.getString("amount", null);
|
||||
if (amnt != null && !amnt.isEmpty()) {
|
||||
try {
|
||||
SwarmAmount amount = SwarmAmount.valueOf(amnt.toUpperCase());
|
||||
result.setAmount(amount);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ConfigError("Failed to parse wave amount for wave " + name + " of arena " + arena.configName() + ": " + amnt);
|
||||
}
|
||||
String amount = config.getString("amount", null);
|
||||
if (amount == null || amount.isEmpty()) {
|
||||
amount = "<initial-players> * 5";
|
||||
} else {
|
||||
result.setAmount(SwarmAmount.LOW);
|
||||
String macro = arena.getPlugin().getFormulaMacros().get("swarm-amount", amount);
|
||||
if (macro != null) {
|
||||
amount = macro;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Formula formula = arena.getPlugin().getFormulaManager().parse(amount);
|
||||
result.setAmount(formula);
|
||||
} catch (IllegalArgumentException e) {
|
||||
String message = String.format("Failed to parse wave amount for wave %s of arena %s: %s\n%s", name, arena.configName(), amount, e.getMessage());
|
||||
throw new ConfigError(message);
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -261,20 +269,21 @@ public class WaveParser
|
||||
}
|
||||
|
||||
// Grab the boss health
|
||||
String healthString = config.getString("health", null);
|
||||
if (healthString != null && !healthString.isEmpty()) {
|
||||
try {
|
||||
BossHealth health = BossHealth.valueOf(healthString.toUpperCase());
|
||||
result.setHealth(health);
|
||||
} catch (IllegalArgumentException e) {
|
||||
int flatHealth = config.getInt("health", -1);
|
||||
if (flatHealth < 0) {
|
||||
throw new ConfigError("Failed to parse boss health for wave " + name + " of arena " + arena.configName() + ": " + healthString);
|
||||
}
|
||||
result.setFlatHealth(flatHealth);
|
||||
}
|
||||
String health = config.getString("health", null);
|
||||
if (health == null || health.isEmpty()) {
|
||||
health = "(<initial-players> + 1) * 20 * 8";
|
||||
} else {
|
||||
result.setHealth(BossHealth.MEDIUM);
|
||||
String macro = arena.getPlugin().getFormulaMacros().get("boss-health", health);
|
||||
if (macro != null) {
|
||||
health = macro;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Formula formula = arena.getPlugin().getFormulaManager().parse(health);
|
||||
result.setHealth(formula);
|
||||
} catch (IllegalArgumentException e) {
|
||||
String message = String.format("Failed to parse boss health for wave %s of arena %s: %s\n%s", name, arena.configName(), health, e.getMessage());
|
||||
throw new ConfigError(message);
|
||||
}
|
||||
|
||||
// And the abilities.
|
||||
@ -588,7 +597,6 @@ public class WaveParser
|
||||
result.setFirstWave(1);
|
||||
result.setPriority(1);
|
||||
result.setFrequency(1);
|
||||
result.setGrowth(WaveGrowth.OLD);
|
||||
result.setHealthMultiplier(1D);
|
||||
result.setAmountMultiplier(1D);
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.garbagemule.MobArena.waves.types;
|
||||
|
||||
import com.garbagemule.MobArena.Msg;
|
||||
import com.garbagemule.MobArena.formula.Formula;
|
||||
import com.garbagemule.MobArena.formula.Formulas;
|
||||
import com.garbagemule.MobArena.framework.Arena;
|
||||
import com.garbagemule.MobArena.things.Thing;
|
||||
import com.garbagemule.MobArena.things.ThingPicker;
|
||||
@ -11,7 +13,6 @@ import com.garbagemule.MobArena.waves.MACreature;
|
||||
import com.garbagemule.MobArena.waves.Wave;
|
||||
import com.garbagemule.MobArena.waves.ability.Ability;
|
||||
import com.garbagemule.MobArena.waves.ability.AbilityInfo;
|
||||
import com.garbagemule.MobArena.waves.enums.BossHealth;
|
||||
import com.garbagemule.MobArena.waves.enums.WaveType;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
@ -29,9 +30,7 @@ public class BossWave extends AbstractWave
|
||||
private MACreature monster;
|
||||
private Set<MABoss> bosses;
|
||||
|
||||
private boolean useHealthMultiplier;
|
||||
private int healthMultiplier;
|
||||
private int flatHealth;
|
||||
private Formula health;
|
||||
|
||||
private List<Ability> abilities;
|
||||
private boolean activated, abilityAnnounce;
|
||||
@ -47,11 +46,8 @@ public class BossWave extends AbstractWave
|
||||
this.abilities = new ArrayList<>();
|
||||
this.activated = false;
|
||||
this.abilityAnnounce = false;
|
||||
this.health = Formulas.DEFAULT_BOSS_HEALTH;
|
||||
this.setType(WaveType.BOSS);
|
||||
|
||||
this.useHealthMultiplier = true;
|
||||
this.healthMultiplier = 0;
|
||||
this.flatHealth = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -69,21 +65,12 @@ public class BossWave extends AbstractWave
|
||||
this.bossName = bossName;
|
||||
}
|
||||
|
||||
public int getMaxHealth(int playerCount) {
|
||||
if (useHealthMultiplier) {
|
||||
return (playerCount + 1) * 20 * healthMultiplier;
|
||||
}
|
||||
return flatHealth;
|
||||
public Formula getHealth() {
|
||||
return health;
|
||||
}
|
||||
|
||||
public void setHealth(BossHealth health) {
|
||||
this.healthMultiplier = health.getMultiplier();
|
||||
this.useHealthMultiplier = true;
|
||||
}
|
||||
|
||||
public void setFlatHealth(int flatHealth) {
|
||||
this.flatHealth = flatHealth;
|
||||
this.useHealthMultiplier = false;
|
||||
public void setHealth(Formula health) {
|
||||
this.health = health;
|
||||
}
|
||||
|
||||
public void addMABoss(MABoss boss) {
|
||||
@ -160,9 +147,7 @@ public class BossWave extends AbstractWave
|
||||
}
|
||||
result.abilityInterval = this.abilityInterval;
|
||||
result.abilityAnnounce = this.abilityAnnounce;
|
||||
result.useHealthMultiplier = this.useHealthMultiplier;
|
||||
result.healthMultiplier = this.healthMultiplier;
|
||||
result.flatHealth = this.flatHealth;
|
||||
result.health = this.health;
|
||||
result.reward = this.reward;
|
||||
result.drops = this.drops;
|
||||
result.bossName = this.bossName;
|
||||
|
@ -1,10 +1,11 @@
|
||||
package com.garbagemule.MobArena.waves.types;
|
||||
|
||||
import com.garbagemule.MobArena.formula.Formula;
|
||||
import com.garbagemule.MobArena.formula.Formulas;
|
||||
import com.garbagemule.MobArena.framework.Arena;
|
||||
import com.garbagemule.MobArena.waves.AbstractWave;
|
||||
import com.garbagemule.MobArena.waves.MACreature;
|
||||
import com.garbagemule.MobArena.waves.Wave;
|
||||
import com.garbagemule.MobArena.waves.enums.WaveGrowth;
|
||||
import com.garbagemule.MobArena.waves.enums.WaveType;
|
||||
|
||||
import java.util.HashMap;
|
||||
@ -15,12 +16,12 @@ import java.util.SortedMap;
|
||||
public class DefaultWave extends AbstractWave
|
||||
{
|
||||
private SortedMap<Integer,MACreature> monsterMap;
|
||||
private WaveGrowth growth;
|
||||
private Formula growth;
|
||||
private boolean fixed;
|
||||
|
||||
public DefaultWave(SortedMap<Integer,MACreature> monsterMap) {
|
||||
this.monsterMap = monsterMap;
|
||||
this.growth = WaveGrowth.OLD;
|
||||
this.growth = Formulas.DEFAULT_WAVE_GROWTH;
|
||||
this.setType(WaveType.DEFAULT);
|
||||
}
|
||||
|
||||
@ -29,7 +30,7 @@ public class DefaultWave extends AbstractWave
|
||||
if (fixed) return getFixed();
|
||||
|
||||
// Get the amount of monsters to spawn.
|
||||
int toSpawn = (int) Math.max(1D, growth.getAmount(wave, playerCount) * super.getAmountMultiplier());
|
||||
int toSpawn = (int) Math.max(1D, (growth.evaluate(arena) * super.getAmountMultiplier()));
|
||||
|
||||
// Grab the total probability sum.
|
||||
int total = monsterMap.lastKey();
|
||||
@ -71,11 +72,11 @@ public class DefaultWave extends AbstractWave
|
||||
return result;
|
||||
}
|
||||
|
||||
public WaveGrowth getGrowth() {
|
||||
public Formula getGrowth() {
|
||||
return growth;
|
||||
}
|
||||
|
||||
public void setGrowth(WaveGrowth growth) {
|
||||
public void setGrowth(Formula growth) {
|
||||
this.growth = growth;
|
||||
this.fixed = false;
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
package com.garbagemule.MobArena.waves.types;
|
||||
|
||||
import com.garbagemule.MobArena.formula.Formula;
|
||||
import com.garbagemule.MobArena.formula.Formulas;
|
||||
import com.garbagemule.MobArena.framework.Arena;
|
||||
import com.garbagemule.MobArena.waves.AbstractWave;
|
||||
import com.garbagemule.MobArena.waves.MACreature;
|
||||
import com.garbagemule.MobArena.waves.Wave;
|
||||
import com.garbagemule.MobArena.waves.enums.SwarmAmount;
|
||||
import com.garbagemule.MobArena.waves.enums.WaveType;
|
||||
|
||||
import java.util.HashMap;
|
||||
@ -13,11 +14,11 @@ import java.util.Map;
|
||||
public class SwarmWave extends AbstractWave
|
||||
{
|
||||
private MACreature monster;
|
||||
private SwarmAmount amount;
|
||||
private Formula amount;
|
||||
|
||||
public SwarmWave(MACreature monster) {
|
||||
this.monster = monster;
|
||||
this.amount = SwarmAmount.LOW;
|
||||
this.amount = Formulas.DEFAULT_SWARM_AMOUNT;
|
||||
this.setType(WaveType.SWARM);
|
||||
}
|
||||
|
||||
@ -27,17 +28,17 @@ public class SwarmWave extends AbstractWave
|
||||
Map<MACreature,Integer> result = new HashMap<>();
|
||||
|
||||
// Add the monster and the swarm amount.
|
||||
int toSpawn = (int) Math.max(1D, amount.getAmount(playerCount) * super.getAmountMultiplier());
|
||||
int toSpawn = (int) Math.max(1D, (amount.evaluate(arena) * super.getAmountMultiplier()));
|
||||
result.put(monster, toSpawn);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public SwarmAmount getAmount() {
|
||||
public Formula getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(SwarmAmount amount) {
|
||||
public void setAmount(Formula amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
|
34
src/main/resources/formulas.yml
Normal file
34
src/main/resources/formulas.yml
Normal file
@ -0,0 +1,34 @@
|
||||
# These formulas work for all properties. If a global formula has the
|
||||
# same name as a property-specific formula, it is the property-specific
|
||||
# formula that will be used when parsing the given property.
|
||||
global:
|
||||
wave-squared: <current-wave> ^ 2
|
||||
wave-inverted: max(1, <final-wave> - <current-wave>)
|
||||
five-each: <live-players> * 5
|
||||
double-team: <live-players> / 2
|
||||
top-up: max(1, 10 - <live-monsters>)
|
||||
dead-man-walking: max(1, <dead-players>)
|
||||
|
||||
# These formulas only work in the "growth" property of Default Waves.
|
||||
wave-growth:
|
||||
old: <current-wave> + <initial-players>
|
||||
slow: min(ceil(<initial-players> / 2) + 1, 13) * <current-wave> ^ 0.5
|
||||
medium: min(ceil(<initial-players> / 2) + 1, 13) * <current-wave> ^ 0.65
|
||||
fast: min(ceil(<initial-players> / 2) + 1, 13) * <current-wave> ^ 0.8
|
||||
psycho: min(ceil(<initial-players> / 2) + 1, 13) * <current-wave> ^ 1.2
|
||||
|
||||
# These formulas only work in the "amount" property of Swarm Waves.
|
||||
swarm-amount:
|
||||
low: max(1, floor(<initial-players> / 2)) * 10
|
||||
medium: max(1, floor(<initial-players> / 2)) * 20
|
||||
high: max(1, floor(<initial-players> / 2)) * 30
|
||||
psycho: max(1, floor(<initial-players> / 2)) * 60
|
||||
|
||||
# These formulas only work in the "health" property of Boss Waves.
|
||||
boss-health:
|
||||
verylow: (<initial-players> + 1) * 20 * 4
|
||||
low: (<initial-players> + 1) * 20 * 8
|
||||
medium: (<initial-players> + 1) * 20 * 15
|
||||
high: (<initial-players> + 1) * 20 * 25
|
||||
veryhigh: (<initial-players> + 1) * 20 * 40
|
||||
psycho: (<initial-players> + 1) * 20 * 60
|
@ -0,0 +1,260 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import com.garbagemule.MobArena.MobArena;
|
||||
import com.garbagemule.MobArena.MonsterManager;
|
||||
import com.garbagemule.MobArena.framework.Arena;
|
||||
import com.garbagemule.MobArena.waves.WaveManager;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.runners.Enclosed;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.MatcherAssert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@RunWith(Enclosed.class)
|
||||
public class FormulaMacrosIT {
|
||||
|
||||
static MobArena plugin;
|
||||
static Arena arena;
|
||||
static FormulaMacros macros;
|
||||
static FormulaManager parser;
|
||||
|
||||
static int finalWave = 13;
|
||||
static int currentWave = finalWave - 2;
|
||||
static int liveMonsters = 9;
|
||||
static int initialPlayers = 7;
|
||||
static int livePlayers = initialPlayers - 2;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() throws IOException {
|
||||
plugin = mock(MobArena.class);
|
||||
File resources = new File("src/main/resources");
|
||||
when(plugin.getDataFolder()).thenReturn(resources);
|
||||
|
||||
arena = mock(Arena.class);
|
||||
WaveManager wm = mock(WaveManager.class);
|
||||
when(wm.getWaveNumber()).thenReturn(currentWave);
|
||||
when(wm.getFinalWave()).thenReturn(finalWave);
|
||||
when(arena.getWaveManager()).thenReturn(wm);
|
||||
Set<LivingEntity> monsters = new HashSet<>();
|
||||
for (int i = 0; i < liveMonsters; i++) {
|
||||
monsters.add(mock(LivingEntity.class));
|
||||
}
|
||||
MonsterManager mm = mock(MonsterManager.class);
|
||||
when(mm.getMonsters()).thenReturn(monsters);
|
||||
when(arena.getMonsterManager()).thenReturn(mm);
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (int i = 0; i < livePlayers; i++) {
|
||||
players.add(mock(Player.class));
|
||||
}
|
||||
when(arena.getPlayersInArena()).thenReturn(players);
|
||||
when(arena.getPlayerCount()).thenReturn(initialPlayers);
|
||||
|
||||
macros = FormulaMacros.create(plugin);
|
||||
macros.reload();
|
||||
|
||||
parser = FormulaManager.createDefault();
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class Global {
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"wave-squared", currentWave * currentWave},
|
||||
{"wave-inverted", finalWave - currentWave},
|
||||
{"five-each", livePlayers * 5},
|
||||
{"double-team", (double) livePlayers / 2},
|
||||
{"top-up", 10 - liveMonsters},
|
||||
{"dead-man-walking", initialPlayers - livePlayers},
|
||||
});
|
||||
}
|
||||
|
||||
String macro;
|
||||
double expected;
|
||||
|
||||
public Global(String macro, double expected) {
|
||||
this.macro = macro;
|
||||
this.expected = expected;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
String value = macros.get("global", macro);
|
||||
Formula formula = parser.parse(value);
|
||||
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class WaveGrowth {
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"slow", 0.5},
|
||||
{"medium", 0.65},
|
||||
{"fast", 0.8},
|
||||
{"psycho", 1.2},
|
||||
});
|
||||
}
|
||||
|
||||
String macro;
|
||||
double exponent;
|
||||
|
||||
public WaveGrowth(String macro, double exponent) {
|
||||
this.macro = macro;
|
||||
this.exponent = exponent;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
String value = macros.get("wave-growth", macro);
|
||||
Formula formula = parser.parse(value);
|
||||
|
||||
double result = (int) formula.evaluate(arena);
|
||||
|
||||
double base = (int) Math.ceil(initialPlayers / 2.0) + 1;
|
||||
double expected = (int) (base * Math.pow(currentWave, exponent));
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Old wave growth formula is different, so we'll just have a
|
||||
* different test for it all-together.
|
||||
*/
|
||||
public static class WaveGrowthOld {
|
||||
|
||||
@Test
|
||||
public void oldWaveGrowth() {
|
||||
String value = macros.get("wave-growth", "old");
|
||||
Formula formula = parser.parse(value);
|
||||
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
double expected = currentWave + initialPlayers;
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class SwarmAmount {
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"low", 10},
|
||||
{"medium", 20},
|
||||
{"high", 30},
|
||||
{"psycho", 60},
|
||||
});
|
||||
}
|
||||
|
||||
String macro;
|
||||
double multiplier;
|
||||
|
||||
public SwarmAmount(String macro, double multiplier) {
|
||||
this.macro = macro;
|
||||
this.multiplier = multiplier;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
String value = macros.get("swarm-amount", macro);
|
||||
Formula formula = parser.parse(value);
|
||||
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
double expected = (double) (initialPlayers / 2) * multiplier;
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class BossHealth {
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"verylow", 4},
|
||||
{"low", 8},
|
||||
{"medium", 15},
|
||||
{"high", 25},
|
||||
{"veryhigh", 40},
|
||||
{"psycho", 60},
|
||||
});
|
||||
}
|
||||
|
||||
String macro;
|
||||
double multiplier;
|
||||
|
||||
public BossHealth(String macro, double multiplier) {
|
||||
this.macro = macro;
|
||||
this.multiplier = multiplier;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
String value = macros.get("boss-health", macro);
|
||||
Formula formula = parser.parse(value);
|
||||
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
double expected = (initialPlayers + 1) * 20 * multiplier;
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Try actually loading a non-default formulas.yml with a couple
|
||||
* of different types of formulas in it to test that the loading
|
||||
* itself actually works.
|
||||
*/
|
||||
public static class TestFile {
|
||||
|
||||
@Test
|
||||
public void loadsUnorthodoxFile() throws IOException {
|
||||
plugin = mock(MobArena.class);
|
||||
File resources = new File("src/test/resources");
|
||||
when(plugin.getDataFolder()).thenReturn(resources);
|
||||
|
||||
FormulaMacros subject = FormulaMacros.create(plugin);
|
||||
subject.reload();
|
||||
|
||||
assertThat(subject.get("numbers", "one"), equalTo("1"));
|
||||
assertThat(subject.get("numbers", "two"), equalTo("2"));
|
||||
assertThat(subject.get("constants", "three-point-one-four"), equalTo("pi"));
|
||||
assertThat(subject.get("constants", "eulers-number"), equalTo("e"));
|
||||
assertThat(subject.get("variables", "live"), equalTo("<live-players>"));
|
||||
assertThat(subject.get("variables", "max"), equalTo("<max-players>"));
|
||||
assertThat(subject.get("operators", "two-plus-two"), equalTo("2 + 2"));
|
||||
assertThat(subject.get("operators", "one-times-two"), equalTo("1 * 2"));
|
||||
assertThat(subject.get("functions", "square-root"), equalTo("sqrt(9)"));
|
||||
assertThat(subject.get("functions", "maximum"), equalTo("max(1, 2)"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,372 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import com.garbagemule.MobArena.MonsterManager;
|
||||
import com.garbagemule.MobArena.framework.Arena;
|
||||
import com.garbagemule.MobArena.waves.WaveManager;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.runners.Enclosed;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.MatcherAssert.*;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@RunWith(Enclosed.class)
|
||||
public class FormulaManagerIT {
|
||||
|
||||
static Arena arena;
|
||||
static FormulaManager subject;
|
||||
|
||||
static int finalWave = 13;
|
||||
static int currentWave = finalWave - 2;
|
||||
static int liveMonsters = 9;
|
||||
static int initialPlayers = 7;
|
||||
static int livePlayers = initialPlayers - 2;
|
||||
static int deadPlayers = initialPlayers - livePlayers;
|
||||
static int minPlayers = 3;
|
||||
static int maxPlayers = initialPlayers + 3;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
arena = mock(Arena.class);
|
||||
WaveManager wm = mock(WaveManager.class);
|
||||
when(wm.getWaveNumber()).thenReturn(currentWave);
|
||||
when(wm.getFinalWave()).thenReturn(finalWave);
|
||||
when(arena.getWaveManager()).thenReturn(wm);
|
||||
Set<LivingEntity> monsters = new HashSet<>();
|
||||
for (int i = 0; i < liveMonsters; i++) {
|
||||
monsters.add(mock(LivingEntity.class));
|
||||
}
|
||||
MonsterManager mm = mock(MonsterManager.class);
|
||||
when(mm.getMonsters()).thenReturn(monsters);
|
||||
when(arena.getMonsterManager()).thenReturn(mm);
|
||||
Set<Player> players = new HashSet<>();
|
||||
for (int i = 0; i < livePlayers; i++) {
|
||||
players.add(mock(Player.class));
|
||||
}
|
||||
when(arena.getPlayersInArena()).thenReturn(players);
|
||||
when(arena.getPlayerCount()).thenReturn(initialPlayers);
|
||||
when(arena.getMinPlayers()).thenReturn(minPlayers);
|
||||
when(arena.getMaxPlayers()).thenReturn(maxPlayers);
|
||||
|
||||
subject = FormulaManager.createDefault();
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class NumberLiterals {
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"0"},
|
||||
{"1"},
|
||||
{"-1"},
|
||||
{"1337"},
|
||||
{"3.14"},
|
||||
{"1e4"},
|
||||
{"-1e4"},
|
||||
{"1e-4"},
|
||||
{"-1e-4"},
|
||||
});
|
||||
}
|
||||
|
||||
String input;
|
||||
double expected;
|
||||
|
||||
public NumberLiterals(String input) {
|
||||
this.input = input;
|
||||
this.expected = Double.parseDouble(input);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
Formula formula = subject.parse(input);
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class DefaultConstants {
|
||||
|
||||
@Parameters(name = "{0} = {1}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"pi", Math.PI},
|
||||
{"e", Math.E},
|
||||
{"pi^e", Math.pow(Math.PI, Math.E)},
|
||||
});
|
||||
}
|
||||
|
||||
String input;
|
||||
double expected;
|
||||
|
||||
public DefaultConstants(String input, double expected) {
|
||||
this.input = input;
|
||||
this.expected = expected;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
Formula formula = subject.parse(input);
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class DefaultVariables {
|
||||
|
||||
@Parameters(name = "{0} = {1}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"<current-wave>", currentWave},
|
||||
{"<final-wave>", finalWave},
|
||||
{"<initial-players>", initialPlayers},
|
||||
{"<live-players>", livePlayers},
|
||||
{"<dead-players>", deadPlayers},
|
||||
{"<min-players>", minPlayers},
|
||||
{"<max-players>", maxPlayers},
|
||||
{"<live-monsters>", liveMonsters},
|
||||
});
|
||||
}
|
||||
|
||||
String input;
|
||||
double expected;
|
||||
|
||||
public DefaultVariables(String input, double expected) {
|
||||
this.input = input;
|
||||
this.expected = expected;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
Formula formula = subject.parse(input);
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* With custom variables, we are manipulating the internal
|
||||
* state of the manager, so we need to use a local subject.
|
||||
*/
|
||||
public static class CustomVariables {
|
||||
|
||||
FormulaManager subject;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
subject = FormulaManager.createDefault();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveRegisteredCustomVariable() {
|
||||
subject.registerVariable("bob", a -> 7.5);
|
||||
|
||||
Formula formula = subject.parse("2.5 + <bob>");
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
double expected = 10;
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throwsOnUnknownCustomVariable() {
|
||||
assertThrows(
|
||||
UnknownToken.class,
|
||||
() -> subject.parse("2 + <bob>")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class DefaultUnaryOperators {
|
||||
|
||||
@Parameters(name = "{0} = {1}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"1 + +1.2", 1 + +1.2},
|
||||
{"1 + -1.2", 1 + -1.2},
|
||||
});
|
||||
}
|
||||
|
||||
String input;
|
||||
double expected;
|
||||
|
||||
public DefaultUnaryOperators(String input, double expected) {
|
||||
this.input = input;
|
||||
this.expected = expected;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
Formula formula = subject.parse(input);
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class DefaultOperators {
|
||||
|
||||
@Parameters(name = "{0} = {1}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"1+-2", 1 + -2},
|
||||
{"3-+4", 3 - +4},
|
||||
{"3*7.5", 3 * 7.5},
|
||||
{"10/2.5", 10 / 2.5},
|
||||
{"9%4", 9 % 4},
|
||||
{"2^-8", Math.pow(2, -8)},
|
||||
{"-2^-8", -Math.pow(2, -8)},
|
||||
{"(-2)^-8", Math.pow(-2, -8)},
|
||||
});
|
||||
}
|
||||
|
||||
String input;
|
||||
double expected;
|
||||
|
||||
public DefaultOperators(String input, double expected) {
|
||||
this.input = input;
|
||||
this.expected = expected;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
Formula formula = subject.parse(input);
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class DefaultUnaryFunctions {
|
||||
|
||||
@Parameters(name = "{0} = {1}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"sqrt(4)", 2},
|
||||
{"sqrt(9)", 3},
|
||||
{"abs(2)", 2},
|
||||
{"abs(-2)", 2},
|
||||
{"ceil(8.2)", 9},
|
||||
{"ceil(8.7)", 9},
|
||||
{"floor(8.2)", 8},
|
||||
{"floor(8.7)", 8},
|
||||
{"round(8.2)", 8},
|
||||
{"round(8.7)", 9},
|
||||
{"sin(pi / 2)", Math.sin(Math.PI / 2)},
|
||||
{"cos(pi / 3)", Math.cos(Math.PI / 3)},
|
||||
{"tan(pi / 4)", Math.tan(Math.PI / 4)},
|
||||
});
|
||||
}
|
||||
|
||||
String input;
|
||||
double expected;
|
||||
|
||||
public DefaultUnaryFunctions(String input, double expected) {
|
||||
this.input = input;
|
||||
this.expected = expected;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
Formula formula = subject.parse(input);
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class DefaultBinaryFunctions {
|
||||
|
||||
@Parameters(name = "{0} = {1}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"min(1, 2)", 1},
|
||||
{"min(2, 1)", 1},
|
||||
{"max(1, 2)", 2},
|
||||
{"max(2, 1)", 2},
|
||||
});
|
||||
}
|
||||
|
||||
String input;
|
||||
double expected;
|
||||
|
||||
public DefaultBinaryFunctions(String input, double expected) {
|
||||
this.input = input;
|
||||
this.expected = expected;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
Formula formula = subject.parse(input);
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* With custom functions, we are manipulating the internal
|
||||
* state of the manager, so we need to use a local subject.
|
||||
*/
|
||||
public static class CustomFunctions {
|
||||
|
||||
FormulaManager subject;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
subject = FormulaManager.createDefault();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveRegisteredCustomFunctions() {
|
||||
subject.registerUnaryFunction("flip", a -> -a);
|
||||
subject.registerBinaryFunction("car", (a, b) -> a);
|
||||
subject.registerBinaryFunction("cdr", (a, b) -> b);
|
||||
|
||||
Formula formula = subject.parse("flip(car(1, 2) + cdr(3, 4))");
|
||||
double result = formula.evaluate(arena);
|
||||
|
||||
double expected = -(1 + 4);
|
||||
assertThat(result, equalTo(expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throwsOnUnknownCustomFunction() {
|
||||
assertThrows(
|
||||
UnknownToken.class,
|
||||
() -> subject.parse("flip(1)")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class LexemeMatcher extends TypeSafeMatcher<Lexeme> {
|
||||
|
||||
private final TokenType type;
|
||||
private final String value;
|
||||
|
||||
private LexemeMatcher(TokenType type, String value) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(Lexeme item) {
|
||||
if (item.token.type != type) {
|
||||
return false;
|
||||
}
|
||||
if (value == null) {
|
||||
return true;
|
||||
}
|
||||
return Objects.equals(item.value, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText(type.name() + " '" + value + "'");
|
||||
}
|
||||
|
||||
public static Matcher<Lexeme> matches(TokenType type, String value) {
|
||||
return new LexemeMatcher(type, value);
|
||||
}
|
||||
|
||||
public static Matcher<Lexeme> matches(TokenType type) {
|
||||
return new LexemeMatcher(type, null);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.runners.Enclosed;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static com.garbagemule.MobArena.formula.LexemeMatcher.matches;
|
||||
import static com.garbagemule.MobArena.formula.TokenType.*;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
@RunWith(Enclosed.class)
|
||||
public class LexerConstantTest {
|
||||
|
||||
static Environment env;
|
||||
static Lexer subject;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class ConstantLiterals {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"pi"},
|
||||
{"e"},
|
||||
});
|
||||
}
|
||||
|
||||
String input;
|
||||
|
||||
public ConstantLiterals(String input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(matches(IDENTIFIER, input)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class InvalidConstants {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownConstant() {
|
||||
assertThrows(
|
||||
UnknownToken.class,
|
||||
() -> subject.tokenize("pie")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.runners.Enclosed;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.garbagemule.MobArena.formula.LexemeMatcher.matches;
|
||||
import static com.garbagemule.MobArena.formula.TokenType.*;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
@RunWith(Enclosed.class)
|
||||
public class LexerFunctionTest {
|
||||
|
||||
static Environment env;
|
||||
static Lexer subject;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class UnaryFunctions {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return asList(new Object[][]{
|
||||
{"sqrt"},
|
||||
{"abs"},
|
||||
{"ceil"},
|
||||
{"floor"},
|
||||
{"round"},
|
||||
{"sin"},
|
||||
{"cos"},
|
||||
{"tan"},
|
||||
});
|
||||
}
|
||||
|
||||
String name;
|
||||
String input;
|
||||
|
||||
public UnaryFunctions(String name) {
|
||||
this.name = name;
|
||||
this.input = name + "(1.2)";
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(IDENTIFIER, name),
|
||||
matches(LEFT_PAREN),
|
||||
matches(NUMBER, "1.2"),
|
||||
matches(RIGHT_PAREN)
|
||||
)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class BinaryFunctions {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return asList(new Object[][]{
|
||||
{"min"},
|
||||
{"max"},
|
||||
});
|
||||
}
|
||||
|
||||
String name;
|
||||
String input;
|
||||
|
||||
public BinaryFunctions(String name) {
|
||||
this.name = name;
|
||||
this.input = name + "(1, 2)";
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(IDENTIFIER, name),
|
||||
matches(LEFT_PAREN),
|
||||
matches(NUMBER, "1"),
|
||||
matches(COMMA),
|
||||
matches(NUMBER, "2"),
|
||||
matches(RIGHT_PAREN)
|
||||
)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class InvalidFunctions {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownFunction() {
|
||||
assertThrows(
|
||||
UnknownToken.class,
|
||||
() -> subject.tokenize("best(1.2)")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.runners.Enclosed;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.garbagemule.MobArena.formula.LexemeMatcher.matches;
|
||||
import static com.garbagemule.MobArena.formula.TokenType.*;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
|
||||
@RunWith(Enclosed.class)
|
||||
public class LexerNumberTest {
|
||||
|
||||
static Environment env;
|
||||
static Lexer subject;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class PositiveNumberLiterals {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return asList(new Object[][]{
|
||||
{"0"},
|
||||
{"1"},
|
||||
{"1337"},
|
||||
{"3.14"},
|
||||
{"1e4"},
|
||||
{"1e-4"},
|
||||
{"1.2e4"},
|
||||
});
|
||||
}
|
||||
|
||||
String input;
|
||||
|
||||
public PositiveNumberLiterals(String input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(matches(NUMBER, input)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class NegativeNumberLiterals {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return asList(new Object[][]{
|
||||
{"-0"},
|
||||
{"-1"},
|
||||
{"-1337"},
|
||||
{"-3.14"},
|
||||
{"-1e4"},
|
||||
{"-1e-4"},
|
||||
{"-1.2e4"},
|
||||
});
|
||||
}
|
||||
|
||||
String input;
|
||||
String number;
|
||||
|
||||
public NegativeNumberLiterals(String input) {
|
||||
this.input = input;
|
||||
this.number = input.substring(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(UNARY_OPERATOR, "-"),
|
||||
matches(NUMBER, number)
|
||||
)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.runners.Enclosed;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static com.garbagemule.MobArena.formula.LexemeMatcher.matches;
|
||||
import static com.garbagemule.MobArena.formula.TokenType.*;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
@RunWith(Enclosed.class)
|
||||
public class LexerOperatorTest {
|
||||
|
||||
static Environment env;
|
||||
static Lexer subject;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class BinaryOperators {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"+"},
|
||||
{"-"},
|
||||
{"*"},
|
||||
{"/"},
|
||||
{"%"},
|
||||
{"^"},
|
||||
});
|
||||
}
|
||||
|
||||
String symbol;
|
||||
String input;
|
||||
|
||||
public BinaryOperators(String symbol) {
|
||||
this.symbol = symbol;
|
||||
this.input = "1" + symbol + "2";
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(NUMBER, "1"),
|
||||
matches(BINARY_OPERATOR, symbol),
|
||||
matches(NUMBER, "2")
|
||||
)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class UnaryOperators {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"+"},
|
||||
{"-"},
|
||||
});
|
||||
}
|
||||
|
||||
String symbol;
|
||||
String input;
|
||||
|
||||
public UnaryOperators(String symbol) {
|
||||
this.symbol = symbol;
|
||||
this.input = symbol + "1";
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(UNARY_OPERATOR, symbol),
|
||||
matches(NUMBER, "1")
|
||||
)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class OperatorAmbiguity {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void longestMatchInfix() {
|
||||
env.registerBinaryOperator("--", 2, true, null);
|
||||
|
||||
List<Lexeme> result = subject.tokenize("1---2");
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(NUMBER, "1"),
|
||||
matches(BINARY_OPERATOR, "--"),
|
||||
matches(UNARY_OPERATOR, "-"),
|
||||
matches(NUMBER, "2")
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void longestMatchPrefix() {
|
||||
env.registerUnaryOperator("--", 4, null);
|
||||
|
||||
List<Lexeme> result = subject.tokenize("1---2");
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(NUMBER, "1"),
|
||||
matches(BINARY_OPERATOR, "-"),
|
||||
matches(UNARY_OPERATOR, "--"),
|
||||
matches(NUMBER, "2")
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void longestMatchBoth() {
|
||||
env.registerUnaryOperator("--", 4, null);
|
||||
env.registerBinaryOperator("--", 2, true, null);
|
||||
|
||||
List<Lexeme> result = subject.tokenize("1---2");
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(NUMBER, "1"),
|
||||
matches(BINARY_OPERATOR, "--"),
|
||||
matches(UNARY_OPERATOR, "-"),
|
||||
matches(NUMBER, "2")
|
||||
)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class InvalidOperators {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidOperator() {
|
||||
assertThrows(
|
||||
LexerError.class,
|
||||
() -> subject.tokenize("1@2")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.runners.Enclosed;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.garbagemule.MobArena.formula.LexemeMatcher.matches;
|
||||
import static com.garbagemule.MobArena.formula.TokenType.*;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
|
||||
@RunWith(Enclosed.class)
|
||||
public class LexerParenthesisTest {
|
||||
|
||||
static Environment env;
|
||||
static Lexer subject;
|
||||
|
||||
public static class ParenthesizedExpressions {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parenthesizedPositiveNumberLiteral() {
|
||||
String input = "(2)";
|
||||
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(LEFT_PAREN),
|
||||
matches(NUMBER, "2"),
|
||||
matches(RIGHT_PAREN)
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parenthesizedNegativeNumberLiteral() {
|
||||
String input = "(-2)";
|
||||
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(LEFT_PAREN),
|
||||
matches(UNARY_OPERATOR, "-"),
|
||||
matches(NUMBER, "2"),
|
||||
matches(RIGHT_PAREN)
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negatedParenthesizedPositiveNumberLiteral() {
|
||||
String input = "-(2)";
|
||||
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(UNARY_OPERATOR, "-"),
|
||||
matches(LEFT_PAREN),
|
||||
matches(NUMBER, "2"),
|
||||
matches(RIGHT_PAREN)
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleExpression() {
|
||||
String input = "(2+3)";
|
||||
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(LEFT_PAREN),
|
||||
matches(NUMBER, "2"),
|
||||
matches(BINARY_OPERATOR, "+"),
|
||||
matches(NUMBER, "3"),
|
||||
matches(RIGHT_PAREN)
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedExpression() {
|
||||
String input = "(((2+3)))";
|
||||
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(LEFT_PAREN),
|
||||
matches(LEFT_PAREN),
|
||||
matches(LEFT_PAREN),
|
||||
matches(NUMBER, "2"),
|
||||
matches(BINARY_OPERATOR, "+"),
|
||||
matches(NUMBER, "3"),
|
||||
matches(RIGHT_PAREN),
|
||||
matches(RIGHT_PAREN),
|
||||
matches(RIGHT_PAREN)
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleExpressions() {
|
||||
String input = "(2+3)*-(4-5)^(6/7)";
|
||||
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(LEFT_PAREN),
|
||||
matches(NUMBER, "2"),
|
||||
matches(BINARY_OPERATOR, "+"),
|
||||
matches(NUMBER, "3"),
|
||||
matches(RIGHT_PAREN),
|
||||
matches(BINARY_OPERATOR, "*"),
|
||||
matches(UNARY_OPERATOR, "-"),
|
||||
matches(LEFT_PAREN),
|
||||
matches(NUMBER, "4"),
|
||||
matches(BINARY_OPERATOR, "-"),
|
||||
matches(NUMBER, "5"),
|
||||
matches(RIGHT_PAREN),
|
||||
matches(BINARY_OPERATOR, "^"),
|
||||
matches(LEFT_PAREN),
|
||||
matches(NUMBER, "6"),
|
||||
matches(BINARY_OPERATOR, "/"),
|
||||
matches(NUMBER, "7"),
|
||||
matches(RIGHT_PAREN)
|
||||
)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.runners.Enclosed;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static com.garbagemule.MobArena.formula.LexemeMatcher.matches;
|
||||
import static com.garbagemule.MobArena.formula.TokenType.*;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
@RunWith(Enclosed.class)
|
||||
public class LexerVariableTest {
|
||||
|
||||
static Environment env;
|
||||
static Lexer subject;
|
||||
|
||||
public static class VariableExpressions {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleVariableExpression() {
|
||||
env.registerVariable("a", null);
|
||||
String input = "<a>";
|
||||
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(matches(VARIABLE, "a")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multiVariableExpression() {
|
||||
env.registerVariable("a", null);
|
||||
env.registerVariable("b", null);
|
||||
String input = "<a>+<b>";
|
||||
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(VARIABLE, "a"),
|
||||
matches(BINARY_OPERATOR, "+"),
|
||||
matches(VARIABLE, "b")
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownVariable() {
|
||||
assertThrows(
|
||||
FormulaError.class,
|
||||
() -> subject.tokenize("<a>")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class InvalidVariables {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"<"},
|
||||
{">"},
|
||||
{"<a"},
|
||||
{"a>"},
|
||||
{"<<a>"},
|
||||
{"<a>>"},
|
||||
{"<a >"},
|
||||
{"< a>"},
|
||||
});
|
||||
}
|
||||
|
||||
String input;
|
||||
|
||||
public InvalidVariables(String input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
env.registerVariable("a", null);
|
||||
assertThrows(
|
||||
LexerError.class,
|
||||
() -> subject.tokenize(input)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.garbagemule.MobArena.formula;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.garbagemule.MobArena.formula.LexemeMatcher.matches;
|
||||
import static com.garbagemule.MobArena.formula.TokenType.*;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
|
||||
public class LexerWhitespaceTest {
|
||||
|
||||
Environment env;
|
||||
Lexer subject;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
env = Environment.createDefault();
|
||||
subject = new Lexer(env);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoresWhitespace() {
|
||||
String input = " 1+ 5 - 2\t ^ \n8";
|
||||
|
||||
List<Lexeme> result = subject.tokenize(input);
|
||||
|
||||
assertThat(result, contains(asList(
|
||||
matches(NUMBER, "1"),
|
||||
matches(BINARY_OPERATOR, "+"),
|
||||
matches(NUMBER, "5"),
|
||||
matches(BINARY_OPERATOR, "-"),
|
||||
matches(NUMBER, "2"),
|
||||
matches(BINARY_OPERATOR, "^"),
|
||||
matches(NUMBER, "8")
|
||||
)));
|
||||
}
|
||||
|
||||
}
|
19
src/test/resources/formulas.yml
Normal file
19
src/test/resources/formulas.yml
Normal file
@ -0,0 +1,19 @@
|
||||
numbers:
|
||||
one: 1
|
||||
two: 2
|
||||
|
||||
constants:
|
||||
three-point-one-four: pi
|
||||
eulers-number: e
|
||||
|
||||
variables:
|
||||
live: <live-players>
|
||||
max: <max-players>
|
||||
|
||||
operators:
|
||||
two-plus-two: 2 + 2
|
||||
one-times-two: 1 * 2
|
||||
|
||||
functions:
|
||||
square-root: sqrt(9)
|
||||
maximum: max(1, 2)
|
Loading…
Reference in New Issue
Block a user