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 22:13:03 +02:00
parent 0da90f3963
commit 9081ec8055
45 changed files with 2940 additions and 72 deletions
@@ -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
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)