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:
Andreas Troelsen 2021-04-07 21:52:49 +02:00
parent 0da90f3963
commit 9081ec8055
45 changed files with 2940 additions and 72 deletions

View File

@ -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.

View File

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

View File

@ -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;
}
}

View File

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

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
package com.garbagemule.MobArena.formula;
import java.util.function.BiFunction;
@FunctionalInterface
public interface BinaryOperation extends BiFunction<Double, Double, Double> {
}

View File

@ -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);
}
}

View 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;
}
}

View File

@ -0,0 +1,9 @@
package com.garbagemule.MobArena.formula;
import com.garbagemule.MobArena.framework.Arena;
public interface Formula {
double evaluate(Arena arena);
}

View File

@ -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);
}
}

View File

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

View File

@ -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);
}
}

View 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
}
}

View 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 + "'";
}
}

View 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);
}
}

View File

@ -0,0 +1,9 @@
package com.garbagemule.MobArena.formula;
class LexerError extends FormulaError {
LexerError(String message, String input, int pos) {
super(message, input, pos);
}
}

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

View File

@ -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);
}
}

View 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 + "'";
}
}
}

View File

@ -0,0 +1,12 @@
package com.garbagemule.MobArena.formula;
enum TokenType {
NUMBER,
IDENTIFIER,
VARIABLE,
UNARY_OPERATOR,
BINARY_OPERATOR,
LEFT_PAREN,
RIGHT_PAREN,
COMMA
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
package com.garbagemule.MobArena.formula;
import java.util.function.Function;
@FunctionalInterface
public interface UnaryOperation extends Function<Double, Double> {
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

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

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View 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

View File

@ -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)"));
}
}
}

View File

@ -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)")
);
}
}
}

View File

@ -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);
}
}

View File

@ -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")
);
}
}
}

View File

@ -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)")
);
}
}
}

View File

@ -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)
)));
}
}
}

View File

@ -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")
);
}
}
}

View File

@ -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)
)));
}
}
}

View File

@ -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)
);
}
}
}

View File

@ -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")
)));
}
}

View 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)