diff --git a/src/main/java/ch/njol/skript/expressions/ExprRandomNumber.java b/src/main/java/ch/njol/skript/expressions/ExprRandomNumber.java index 9cf189d5643..f9ae82447fd 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprRandomNumber.java +++ b/src/main/java/ch/njol/skript/expressions/ExprRandomNumber.java @@ -18,9 +18,11 @@ */ package ch.njol.skript.expressions; +import java.util.Arrays; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; +import ch.njol.skript.lang.Literal; import ch.njol.util.Math2; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; @@ -36,62 +38,90 @@ import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; -@Name("Random Number") +@Name("Random Numbers") @Description({ - "A random number or integer between two given numbers. Use 'number' if you want any number with decimal parts, or use use 'integer' if you only want whole numbers.", - "Please note that the order of the numbers doesn't matter, i.e. random number between 2 and 1 will work as well as random number between 1 and 2." + "A given amount of random numbers or integers between two given numbers. Use 'number' if you want any number with decimal parts, or use use 'integer' if you only want whole numbers.", + "Please note that the order of the numbers doesn't matter, i.e. random number between 2 and 1 will work as well as random number between 1 and 2." }) @Examples({ - "set the player's health to a random number between 5 and 10", - "send \"You rolled a %random integer from 1 to 6%!\" to the player" + "set the player's health to a random number between 5 and 10", + "send \"You rolled a %random integer from 1 to 6%!\" to the player", + "set {_chances::*} to 5 random integers between 5 and 96", + "set {_decimals::*} to 3 random numbers between 2.7 and -1.5" }) -@Since("1.4") +@Since("1.4, INSERT VERSION (Multiple random numbers)") public class ExprRandomNumber extends SimpleExpression { static { Skript.registerExpression(ExprRandomNumber.class, Number.class, ExpressionType.COMBINED, - "[a] random (:integer|number) (from|between) %number% (to|and) %number%"); + "[a|%-integer%] random (:integer|number)[s] (from|between) %number% (to|and) %number%"); } - private Expression from, to; + @Nullable + private Expression amount; + private Expression lower, upper; private boolean isInteger; @Override @SuppressWarnings("unchecked") - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parser) { - from = (Expression) exprs[0]; - to = (Expression) exprs[1]; - isInteger = parser.hasTag("integer"); + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + amount = (Expression) exprs[0]; + lower = (Expression) exprs[1]; + upper = (Expression) exprs[2]; + isInteger = parseResult.hasTag("integer"); return true; } @Override @Nullable protected Number[] get(Event event) { - Number from = this.from.getSingle(event); - Number to = this.to.getSingle(event); + Number lowerNumber = lower.getSingle(event); + Number upperNumber = upper.getSingle(event); + if (upperNumber == null || lowerNumber == null || !Double.isFinite(lowerNumber.doubleValue()) || !Double.isFinite(upperNumber.doubleValue())) + return new Number[0]; - if (to == null || from == null || !Double.isFinite(from.doubleValue()) || !Double.isFinite(to.doubleValue())) + Integer amount = this.amount == null ? Integer.valueOf(1) : this.amount.getSingle(event); + if (amount == null || amount <= 0) return new Number[0]; + double lower = Math.min(lowerNumber.doubleValue(), upperNumber.doubleValue()); + double upper = Math.max(lowerNumber.doubleValue(), upperNumber.doubleValue()); Random random = ThreadLocalRandom.current(); - double min = Math.min(from.doubleValue(), to.doubleValue()); - double max = Math.max(from.doubleValue(), to.doubleValue()); - if (isInteger) { - long inf = Math2.ceil(min); - long sup = Math2.floor(max); - if (max - min < 1 && inf - sup <= 1) { - if (sup == inf || min == inf) - return new Long[] {inf}; - if (max == sup) - return new Long[] {sup}; + Long[] longs = new Long[amount]; + long floored_upper = Math2.floor(upper); + long ceiled_lower = Math2.ceil(lower); + + // catch issues like `integer between 0.5 and 0.6` + if (upper - lower < 1 && ceiled_lower - floored_upper <= 1) { + if (floored_upper == ceiled_lower || lower == ceiled_lower) { + Arrays.fill(longs, ceiled_lower); + return longs; + } + if (upper == floored_upper) { + Arrays.fill(longs, floored_upper); + return longs; + } return new Long[0]; } - return new Long[] {inf + Math2.mod(random.nextLong(), sup - inf + 1)}; + + for (int i = 0; i < amount; i++) + longs[i] = Math2.ceil(lower) + Math2.mod(random.nextLong(), floored_upper - ceiled_lower + 1); + return longs; + // non-integers + } else { + Double[] doubles = new Double[amount]; + for (int i = 0; i < amount; i++) + doubles[i] = Math.min(lower + random.nextDouble() * (upper - lower), upper); + return doubles; } + } - return new Double[] {min + random.nextDouble() * (max - min)}; + @Override + public boolean isSingle() { + if (amount instanceof Literal) + return ((Literal) amount).getSingle() == 1; + return amount == null; } @Override @@ -101,12 +131,8 @@ public Class getReturnType() { @Override public String toString(@Nullable Event event, boolean debug) { - return "a random " + (isInteger ? "integer" : "number") + " between " + from.toString(event, debug) + " and " + to.toString(event, debug); - } - - @Override - public boolean isSingle() { - return true; + return (amount == null ? "a" : amount.toString(event, debug)) + " random " + (isInteger ? "integer" : "number") + + (amount == null ? "" : "s") + " between " + lower.toString(event, debug) + " and " + upper.toString(event, debug); } } diff --git a/src/test/skript/tests/syntaxes/expressions/ExprRandomNumber.sk b/src/test/skript/tests/syntaxes/expressions/ExprRandomNumber.sk new file mode 100644 index 00000000000..45ad642b288 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprRandomNumber.sk @@ -0,0 +1,72 @@ +test "random numbers": + loop 1000 times: + set {_num} to random number between 1 and 2.5 + assert {_num} is set with "(1) failed to generate random number: %{_num}%" + assert {_num} is between 1 and 2.5 with "(1) random number outside bounds: %{_num}%" + + set {_int} to random integer between 1 and 2.5 + assert {_int} is set with "(2) failed to generate random int: %{_int}%" + assert {_int} is between 1 and 2.5 with "(2) random int outside bounds: %{_int}%" + assert {_int} is 1 or 2 with "(2) random int generated an invalid result: %{_int}%" + + # negatives + set {_num} to random number between 1 and -2.5 + assert {_num} is set with "(3) failed to generate random number: %{_num}%" + assert {_num} is between 1 and -2.5 with "(3) random number outside bounds: %{_num}%" + + set {_int} to random integer between 1 and -2.5 + assert {_int} is set with "(4) failed to generate random int: %{_int}%" + assert {_int} is between 1 and -2.5 with "(4) random int outside bounds: %{_int}%" + assert {_int} is 1 or 0 or -1 or -2 with "(4) random int generated an invalid result: %{_int}%" + + # multiple values + set {_nums::*} to 3 random numbers between 1 and -2.5 + assert {_nums::*} is set with "(5) failed to generate random number: %{_nums::*}%" + assert size of {_nums::*} is 3 with "(6) random numbers generated the wrong amount of results: %size of {_nums::*}% != 3" + assert {_nums::*} is between 1 and -2.5 with "(5) random number outside bounds: %{_nums::*}%" + + set {_ints::*} to 3 random integers between 1 and -2.5 + assert {_ints::*} is set with "(6) failed to generate random int: %{_ints::*}%" + assert size of {_ints::*} is 3 with "(6) random ints generated the wrong amount of results: %size of {_ints::*}% != 3" + assert {_ints::*} are between 1 and -2.5 with "(6) random int outside bounds: %{_ints::*}%" + assert {_ints::*} are 1 or 0 or -1 or -2 with "(6) random int generated an invalid result: %{_ints::*}%" + + # null checks + set {_num} to random number between {_null} and 1 + assert {_num} is not set with "(7) random number with null bound returned non-null result: %{_num}%" + set {_num} to random number between 0 and {_null} + assert {_num} is not set with "(8) random number with null bound returned non-null result: %{_num}%" + set {_num::*} to {_null} random numbers between 0 and 10 + assert {_num::*} is not set with "(9) random number with null amount returned non-null result: %{_num::*}%" + + set {_int} to random integer between {_null} and 1 + assert {_int} is not set with "(10) random integer with null bound returned non-null result: %{_int}%" + set {_int} to random integer between 0 and {_null} + assert {_int} is not set with "(11) random integer with null bound returned non-null result: %{_int}%" + set {_int::*} to {_null} random integers between 0 and 10 + assert {_int::*} is not set with "(12) random integer with null bound returned non-null result: %{_int::*}%" + + # edge cases + set {_num} to random number between 0 and 0 + assert {_num} is 0 with "(13) random number between 0 and 0 returned non-zero result: %{_num}%" + set {_int} to random integer between 0 and 0 + assert {_int} is 0 with "(14) random integer between 0 and 0 returned non-zero result: %{_int}%" + + set {_int} to random integer between 0.5 and 0.6 + assert {_int} is not set with "(15) random integer between 0.5 and 0.6 returned non-null result: %{_int}%" + + set {_int::*} to -10 random integers between 1 and 10 + assert {_int::*} is not set with "(16) -10 random integers returned non-null result: %{_int::*}%" + + # NaN/infinity + set {_int} to random integer between 10 and NaN value + assert {_int} is not set with "(17) random integer between 10 and NaN value returned non-null result: %{_int}%" + + set {_int} to random integer between 10 and infinity value + assert {_int} is not set with "(18) random integer between 10 and infinity value returned non-null result: %{_int}%" + + set {_int} to random integer between 10 and -infinity value + assert {_int} is not set with "(19) random integer between 10 and -infinity value returned non-null result: %{_int}%" + + set {_int::*} to NaN value random integers between 1 and 10 + assert {_int::*} is not set with "(20) NaN value random integers returned non-null result: %{_int}%"