Fixed probability bug Fixes #5055 Fixes #5054

This commit is contained in:
nossr50 2024-07-27 15:01:28 -07:00
parent 248116df21
commit e886a16388
5 changed files with 125 additions and 44 deletions

View File

@ -1,3 +1,10 @@
Version 2.2.018
Fixed a probability bug where certain skills would max out in chance to succeed well before they were supposed to (such as Dodge)
(Codebase) Added more unit tests for Probability and RNG
NOTES:
This probability bug was a big oopsie and showed a gap in unit test coverage, I've added that coverage and a bug like this in theory shouldn't happen again.
Version 2.2.017 Version 2.2.017
Fixed a bug with default Mace permissions (thanks SrBedrock) Fixed a bug with default Mace permissions (thanks SrBedrock)
Fixed Blast Mining being almost completely broken Fixed Blast Mining being almost completely broken

View File

@ -59,9 +59,6 @@ public class AcrobaticsCommand extends SkillCommand {
messages.add(getStatMessage(SubSkillType.ACROBATICS_ROLL, rollStrings[0]) messages.add(getStatMessage(SubSkillType.ACROBATICS_ROLL, rollStrings[0])
+ (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", rollStrings[1]) : "")); + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", rollStrings[1]) : ""));
/*messages.add(getStatMessage(true, false, SubSkillType.ACROBATICS_ROLL, String.valueOf(graceChance))
+ (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", String.valueOf(graceChanceLucky)) : ""));*/
} }
} }

View File

@ -30,6 +30,8 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
public abstract class SkillCommand implements TabExecutor { public abstract class SkillCommand implements TabExecutor {
public static final String ABILITY_GENERIC_TEMPLATE_CUSTOM = "Ability.Generic.Template.Custom";
public static final String ABILITY_GENERIC_TEMPLATE = "Ability.Generic.Template";
protected PrimarySkillType skill; protected PrimarySkillType skill;
protected DecimalFormat percent = new DecimalFormat("##0.00%"); protected DecimalFormat percent = new DecimalFormat("##0.00%");
@ -163,8 +165,6 @@ public abstract class SkillCommand implements TabExecutor {
/* /*
* CHILD SKILLS * CHILD SKILLS
*/ */
var parents = mcMMO.p.getSkillTools().getChildSkillParents(skill); var parents = mcMMO.p.getSkillTools().getChildSkillParents(skill);
//TODO: Add JSON here //TODO: Add JSON here
@ -186,28 +186,7 @@ public abstract class SkillCommand implements TabExecutor {
player.sendMessage(LocaleLoader.getString("Commands.XPGain.Overhaul", LocaleLoader.getString("Commands.XPGain.Child"))); player.sendMessage(LocaleLoader.getString("Commands.XPGain.Overhaul", LocaleLoader.getString("Commands.XPGain.Child")));
player.sendMessage(LocaleLoader.getString("Effects.Child.Overhaul", skillValue, parentMessage.toString())); player.sendMessage(LocaleLoader.getString("Effects.Child.Overhaul", skillValue, parentMessage.toString()));
//LEVEL
//player.sendMessage(LocaleLoader.getString("Effects.Child.Overhaul", skillValue, skillValue));
} }
/*
if (!SkillTools.isChildSkill(skill)) {
player.sendMessage(LocaleLoader.getString("Skills.Header", skillName));
player.sendMessage(LocaleLoader.getString("Commands.XPGain", LocaleLoader.getString("Commands.XPGain." + StringUtils.getCapitalized(skill.toString()))));
player.sendMessage(LocaleLoader.getString("Effects.Level", skillValue, mcMMOPlayer.getSkillXpLevel(skill), mcMMOPlayer.getXpToLevel(skill)));
} else {
player.sendMessage(LocaleLoader.getString("Skills.Header", skillName + " " + LocaleLoader.getString("Skills.Child")));
player.sendMessage(LocaleLoader.getString("Commands.XPGain", LocaleLoader.getString("Commands.XPGain.Child")));
player.sendMessage(LocaleLoader.getString("Effects.Child", skillValue));
player.sendMessage(LocaleLoader.getString("Skills.Header", LocaleLoader.getString("Skills.Parents")));
Set<PrimarySkillType> parents = FamilyTree.getParents(skill);
for (PrimarySkillType parent : parents) {
player.sendMessage(parent.getName() + " - " + LocaleLoader.getString("Effects.Level", mcMMOPlayer.getSkillLevel(parent), mcMMOPlayer.getSkillXpLevel(parent), mcMMOPlayer.getXpToLevel(parent)));
}
}
*/
} }
@Override @Override
@ -222,10 +201,6 @@ public abstract class SkillCommand implements TabExecutor {
return Math.min((int) skillValue, maxLevel) / rankChangeLevel; return Math.min((int) skillValue, maxLevel) / rankChangeLevel;
} }
// protected String[] getAbilityDisplayValues(SkillActivationType skillActivationType, Player player, SubSkillType subSkill) {
// return RandomChanceUtil.calculateAbilityDisplayValues(skillActivationType, player, subSkill);
// }
protected String[] calculateLengthDisplayValues(Player player, float skillValue) { protected String[] calculateLengthDisplayValues(Player player, float skillValue) {
int maxLength = mcMMO.p.getSkillTools().getSuperAbilityMaxLength(mcMMO.p.getSkillTools().getSuperAbility(skill)); int maxLength = mcMMO.p.getSkillTools().getSuperAbilityMaxLength(mcMMO.p.getSkillTools().getSuperAbility(skill));
int abilityLengthVar = mcMMO.p.getAdvancedConfig().getAbilityLength(); int abilityLengthVar = mcMMO.p.getAdvancedConfig().getAbilityLength();
@ -252,14 +227,19 @@ public abstract class SkillCommand implements TabExecutor {
return getStatMessage(false, false, subSkillType, vars); return getStatMessage(false, false, subSkillType, vars);
} }
protected String getStatMessage(boolean isExtra, boolean isCustom, SubSkillType subSkillType, String... vars) { protected String getStatMessage(boolean isExtra, boolean isCustom,
String templateKey = isCustom ? "Ability.Generic.Template.Custom" : "Ability.Generic.Template"; @NotNull SubSkillType subSkillType, String... vars) {
String statDescriptionKey = !isExtra ? subSkillType.getLocaleKeyStatDescription() : subSkillType.getLocaleKeyStatExtraDescription(); final String templateKey = isCustom ? ABILITY_GENERIC_TEMPLATE_CUSTOM : ABILITY_GENERIC_TEMPLATE;
final String statDescriptionKey = !isExtra
? subSkillType.getLocaleKeyStatDescription()
: subSkillType.getLocaleKeyStatExtraDescription();
if (isCustom) if (isCustom) {
return LocaleLoader.getString(templateKey, LocaleLoader.getString(statDescriptionKey, vars)); return LocaleLoader.getString(templateKey, LocaleLoader.getString(statDescriptionKey, vars));
else { } else {
String[] mergedList = NotificationManager.addItemToFirstPositionOfArray(LocaleLoader.getString(statDescriptionKey), vars); final String[] mergedList
= NotificationManager.addItemToFirstPositionOfArray(
LocaleLoader.getString(statDescriptionKey), vars);
return LocaleLoader.getString(templateKey, mergedList); return LocaleLoader.getString(templateKey, mergedList);
} }
} }
@ -276,8 +256,6 @@ public abstract class SkillCommand implements TabExecutor {
protected abstract void permissionsCheck(Player player); protected abstract void permissionsCheck(Player player);
//protected abstract List<String> effectsDisplay();
protected abstract List<String> statsDisplay(Player player, float skillValue, boolean hasEndurance, boolean isLucky); protected abstract List<String> statsDisplay(Player player, float skillValue, boolean hasEndurance, boolean isLucky);
protected abstract List<Component> getTextComponents(Player player); protected abstract List<Component> getTextComponents(Player player);

View File

@ -41,7 +41,7 @@ public class ProbabilityUtil {
public static double chanceOfSuccessPercentage(@Nullable McMMOPlayer mmoPlayer, public static double chanceOfSuccessPercentage(@Nullable McMMOPlayer mmoPlayer,
@NotNull SubSkillType subSkillType, @NotNull SubSkillType subSkillType,
boolean isLucky) { boolean isLucky) {
Probability probability = getSubSkillProbability(subSkillType, mmoPlayer); final Probability probability = getSubSkillProbability(subSkillType, mmoPlayer);
//Probability values are on a 0-1 scale and need to be "transformed" into a 1-100 scale //Probability values are on a 0-1 scale and need to be "transformed" into a 1-100 scale
double percentageValue = probability.getValue(); //Doesn't need to be scaled double percentageValue = probability.getValue(); //Doesn't need to be scaled
@ -437,7 +437,7 @@ public class ProbabilityUtil {
return Probability.ofPercent(ceiling); return Probability.ofPercent(ceiling);
} }
double odds = (skillLevel / maxBonusLevel) * 100D; double odds = ((skillLevel / maxBonusLevel) * ceiling);
// make sure the odds aren't lower or higher than the floor or ceiling // make sure the odds aren't lower or higher than the floor or ceiling
return Probability.ofPercent(Math.min(Math.max(floor, odds), ceiling)); return Probability.ofPercent(Math.min(Math.max(floor, odds), ceiling));

View File

@ -12,6 +12,8 @@ import org.junit.jupiter.params.provider.MethodSource;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Stream; import java.util.stream.Stream;
import static com.gmail.nossr50.datatypes.skills.PrimarySkillType.ACROBATICS;
import static com.gmail.nossr50.datatypes.skills.PrimarySkillType.MINING;
import static com.gmail.nossr50.datatypes.skills.SubSkillType.*; import static com.gmail.nossr50.datatypes.skills.SubSkillType.*;
import static com.gmail.nossr50.util.random.ProbabilityTestUtils.assertProbabilityExpectations; import static com.gmail.nossr50.util.random.ProbabilityTestUtils.assertProbabilityExpectations;
import static com.gmail.nossr50.util.random.ProbabilityUtil.calculateCurrentSkillProbability; import static com.gmail.nossr50.util.random.ProbabilityUtil.calculateCurrentSkillProbability;
@ -68,9 +70,106 @@ class ProbabilityUtilTest extends MMOTestEnvironment {
assertProbabilityExpectations(20, probability); assertProbabilityExpectations(20, probability);
} }
private static Stream<Arguments> provideSkillProbabilityTestData() {
return Stream.of(
// skillLevel, floor, ceiling, maxBonusLevel, expectedValue
// 20% chance at skill level 1000
Arguments.of(1000, 0, 20, 1000, 0.2),
// 10% chance at skill level 500
Arguments.of(500, 0, 20, 1000, 0.1),
// 5% chance at skill level 250
Arguments.of(250, 0, 20, 1000, 0.05),
// 0% chance at skill level 0
Arguments.of(0, 0, 20, 1000, 0.0),
// 0% chance at skill level 1000
Arguments.of(1000, 0, 0, 1000, 0.0),
// 1% chance at skill level 1000
Arguments.of(1000, 0, 1, 1000, 0.01)
);
}
@ParameterizedTest
@MethodSource("provideSkillProbabilityTestData")
void testCalculateCurrentSkillProbability(double skillLevel, double floor, double ceiling, double maxBonusLevel,
double expectedValue) {
// When
final Probability probability = calculateCurrentSkillProbability(skillLevel, floor, ceiling, maxBonusLevel);
// Then
assertEquals(expectedValue, probability.getValue());
}
@Test @Test
public void calculateCurrentSkillProbabilityShouldBeTwenty() { public void getRNGDisplayValuesShouldReturn10PercentForDodge() {
final Probability probability = calculateCurrentSkillProbability(1000, 0, 20, 1000); // Given
assertEquals(0.2D, probability.getValue()); when(advancedConfig.getMaximumProbability(ACROBATICS_DODGE)).thenReturn(20D);
when(advancedConfig.getMaxBonusLevel(ACROBATICS_DODGE)).thenReturn(1000);
mmoPlayer.modifySkill(ACROBATICS, 500);
// When & Then
final String[] rngDisplayValues = ProbabilityUtil.getRNGDisplayValues(mmoPlayer, ACROBATICS_DODGE);
assertEquals("10.00%", rngDisplayValues[0]);
} }
@Test
public void getRNGDisplayValuesShouldReturn20PercentForDodge() {
// Given
when(advancedConfig.getMaximumProbability(ACROBATICS_DODGE)).thenReturn(20D);
when(advancedConfig.getMaxBonusLevel(ACROBATICS_DODGE)).thenReturn(1000);
mmoPlayer.modifySkill(ACROBATICS, 1000);
// When & then
final String[] rngDisplayValues = ProbabilityUtil.getRNGDisplayValues(mmoPlayer, ACROBATICS_DODGE);
assertEquals("20.00%", rngDisplayValues[0]);
}
@Test
public void getRNGDisplayValuesShouldReturn0PercentForDodge() {
// Given
when(advancedConfig.getMaximumProbability(ACROBATICS_DODGE)).thenReturn(20D);
when(advancedConfig.getMaxBonusLevel(ACROBATICS_DODGE)).thenReturn(1000);
mmoPlayer.modifySkill(ACROBATICS, 0);
// When & then
final String[] rngDisplayValues = ProbabilityUtil.getRNGDisplayValues(mmoPlayer, ACROBATICS_DODGE);
assertEquals("0.00%", rngDisplayValues[0]);
}
@Test
public void getRNGDisplayValuesShouldReturn10PercentForDoubleDrops() {
// Given
when(advancedConfig.getMaximumProbability(MINING_DOUBLE_DROPS)).thenReturn(100D);
when(advancedConfig.getMaxBonusLevel(MINING_DOUBLE_DROPS)).thenReturn(1000);
mmoPlayer.modifySkill(MINING, 100);
// When & Then
final String[] rngDisplayValues = ProbabilityUtil.getRNGDisplayValues(mmoPlayer, MINING_DOUBLE_DROPS);
assertEquals("10.00%", rngDisplayValues[0]);
}
@Test
public void getRNGDisplayValuesShouldReturn50PercentForDoubleDrops() {
// Given
when(advancedConfig.getMaximumProbability(MINING_DOUBLE_DROPS)).thenReturn(100D);
when(advancedConfig.getMaxBonusLevel(MINING_DOUBLE_DROPS)).thenReturn(1000);
mmoPlayer.modifySkill(MINING, 500);
// When & Then
final String[] rngDisplayValues = ProbabilityUtil.getRNGDisplayValues(mmoPlayer, MINING_DOUBLE_DROPS);
assertEquals("50.00%", rngDisplayValues[0]);
}
@Test
public void getRNGDisplayValuesShouldReturn100PercentForDoubleDrops() {
// Given
when(advancedConfig.getMaximumProbability(MINING_DOUBLE_DROPS)).thenReturn(100D);
when(advancedConfig.getMaxBonusLevel(MINING_DOUBLE_DROPS)).thenReturn(1000);
mmoPlayer.modifySkill(MINING, 1000);
// When & Then
final String[] rngDisplayValues = ProbabilityUtil.getRNGDisplayValues(mmoPlayer, MINING_DOUBLE_DROPS);
assertEquals("100.00%", rngDisplayValues[0]);
}
} }