I have created a tool that lets me calculate the profitability of doing certain actions in an MMORPG game which I play, it's more edging towards a quick-and-dirty solution than towards a neat solution because it's simply a tool and does not need excessive robustness, however I do think there is room for improvements.
I'd like a review on all aspects, first I'll start of by explaining what it is really about as some domain knowledge is required.
Domain knowledge
This tool focuses on items of 29 Defensive Rating (DR) as input and it is all about upgrading the items, ultimately the goal is to produce a 32 DR item, which can be sold for a fair amount of money.
The item comes with an upgrade of level 0, and for these practices it needs to be upgraded to level 7, though it can be upgraded beyond in the game, here is a table with characteristics:
- Upgrade from 0 to 1: Costs 141,000, 100% regular chance
- Upgrade from 1 to 2: Costs 141,000, 100% regular chance
- Upgrade from 2 to 3: Costs 141,000, 100% regular chance
- Upgrade from 3 to 4: Costs 141,000, 100% regular chance
- Upgrade from 4 to 5: Costs 141,000, 50% regular chance
- Upgrade from 5 to 6: Costs 211,500, 25% regular chance
- Upgrade from 6 to 7: Costs 282,000, 12.5% regular chance
When an item succeeds to 7, it can be converted to a "32DR crystal", which can be sold.
When an item fails, it will produce 1~15 Dazzling Ores and a Level 100 Chip. If the upgrade that failed was from 6 to 7, it will produce 2 Level 100 Chips. Upon failure the item is lost.
To increase upgrade success, you can add 500 Shiny Crystals from upgrade from 4 to 5 onwards, which will double your success chance.
My goal is to calculate the most profitable method (possibly negative profit, also called loss) given the price of the 29 DR item.
Implementation-wise, I have decided for now to let it run on the console for ease of use and with a few variable parameters hard-coded. they resemble the price/value of an item in the game and that, because it is dependent upon players, may vary from day to day. It's just for personal use, so I do not think it is bad to recompile it every day.
In the future I want to support multiple types of items and goals (upgrade levels) though, where the following details are variable:
- The upgrade costs
- The maximum amount of Shiny Crystals needed for upgrading
- The minimum and maximum amount of Dazzling Ores the item produces upon failure.
This is my code:
public class GE32Tool {
/* Variable data */
private final static int DAZZLING_ORE_PRICE = 120_000;
private final static int CRYSTAL_32DR_PRICE = 45_000_000;
private final static int LVL_100_CHIP_PRICE = 190_000;
private final static int SHINY_CRYSTAL_PRICE = 6_000;
/* Constant data */
private final static int UPGRADE_MAXIMUM = 7;
private final static int SHINY_CRYSTAL_MAXIMUM = 500;
private final static int[] BASE_UPGRADE_COSTS = {
141_000, //to 1
141_000, //to 2
141_000, //to 3
141_000, //to 4
141_000, //to 5
211_500, //to 6
282_000, //to 7
};
private final static float[] BASE_UPGRADE_SUCCESS = {
1, //to 1
1, //to 2
1, //to 3
1, //to 4
0.5f, //to 5
0.25f, //to 6
0.125f, //to 7
};
private final static int[] UPGRADE_FAILURE_CHIPS = {
1, //to 1
1, //to 2
1, //to 3
1, //to 4
1, //to 5
1, //to 6
2, //to 7
};
private final static int UPGRADE_DAZZLING_ORE_MIN = 1;
private final static int UPGRADE_DAZZLING_ORE_MAX = 15;
private final static float UPGRADE_DAZZLING_ORE_AVG = (UPGRADE_DAZZLING_ORE_MIN + UPGRADE_DAZZLING_ORE_MAX) / 2f;
private void calculate() {
Scanner scanner = new Scanner(System.in);
System.out.print("Le Noir price: ");
int leNoirPrice = scanner.nextInt();
System.out.println();
int initialCost = leNoirPrice + Arrays.stream(BASE_UPGRADE_COSTS, 0, 4).sum();
Map<String, Float> costs = new HashMap<>();
addToMap(costs, false, false, false);
addToMap(costs, false, false, true);
addToMap(costs, false, true, false);
addToMap(costs, false, true, true);
addToMap(costs, true, false, false);
addToMap(costs, true, false, true);
addToMap(costs, true, true, false);
addToMap(costs, true, true, true);
Map.Entry<String, Float> bestResult = costs.entrySet().stream()
.sorted(Comparator.comparingDouble(entry -> entry.getValue()))
.findFirst()
.get();
float finalProfit = -(initialCost + bestResult.getValue());
System.out.println(String.format("Expected profit: %.2f", finalProfit));
System.out.println("Using " + bestResult.getKey());
}
private void addToMap(final Map<String, Float> costs, final boolean upgrade5ShinyCrystals, final boolean upgrade6ShinyCrystals, final boolean upgrade7ShinyCrystals) {
costs.put("5=" + upgrade5ShinyCrystals + " / 6=" + upgrade6ShinyCrystals + " / 7=" + upgrade7ShinyCrystals,
calculateUpgradeCosts(
upgradeCostsFor(upgrade5ShinyCrystals, upgrade6ShinyCrystals, upgrade7ShinyCrystals),
upgradeSuccessFor(upgrade5ShinyCrystals, upgrade6ShinyCrystals, upgrade7ShinyCrystals),
5
)
);
}
private float[] upgradeSuccessFor(final boolean upgrade5ShinyCrystals, final boolean upgrade6ShinyCrystals, final boolean upgrade7ShinyCrystals) {
return new float[]{
BASE_UPGRADE_SUCCESS[0],
BASE_UPGRADE_SUCCESS[1],
BASE_UPGRADE_SUCCESS[2],
BASE_UPGRADE_SUCCESS[3],
BASE_UPGRADE_SUCCESS[4] + (upgrade5ShinyCrystals ? BASE_UPGRADE_SUCCESS[4] : 0),
BASE_UPGRADE_SUCCESS[5] + (upgrade6ShinyCrystals ? BASE_UPGRADE_SUCCESS[5] : 0),
BASE_UPGRADE_SUCCESS[6] + (upgrade7ShinyCrystals ? BASE_UPGRADE_SUCCESS[6] : 0),
};
}
private int[] upgradeCostsFor(final boolean upgrade5ShinyCrystals, final boolean upgrade6ShinyCrystals, final boolean upgrade7ShinyCrystals) {
return new int[]{
BASE_UPGRADE_COSTS[0],
BASE_UPGRADE_COSTS[1],
BASE_UPGRADE_COSTS[2],
BASE_UPGRADE_COSTS[3],
BASE_UPGRADE_COSTS[4] + (upgrade5ShinyCrystals ? SHINY_CRYSTAL_MAXIMUM * SHINY_CRYSTAL_PRICE : 0),
BASE_UPGRADE_COSTS[5] + (upgrade6ShinyCrystals ? SHINY_CRYSTAL_MAXIMUM * SHINY_CRYSTAL_PRICE : 0),
BASE_UPGRADE_COSTS[6] + (upgrade7ShinyCrystals ? SHINY_CRYSTAL_MAXIMUM * SHINY_CRYSTAL_PRICE : 0),
};
}
private float calculateUpgradeCosts(final int[] upgradeCosts, final float[] upgradeSuccess, final int upgradeLevel) {
if (upgradeLevel == UPGRADE_MAXIMUM + 1) {
return -CRYSTAL_32DR_PRICE;
}
float successChance = upgradeSuccess[upgradeLevel - 1];
float successCost = successChance *
(upgradeCosts[upgradeLevel - 1] + calculateUpgradeCosts(upgradeCosts, upgradeSuccess, upgradeLevel + 1));
float failureCost = (1f - successChance) *
(upgradeCosts[upgradeLevel - 1] - (UPGRADE_DAZZLING_ORE_AVG * DAZZLING_ORE_PRICE) - (UPGRADE_FAILURE_CHIPS[upgradeLevel - 1] * LVL_100_CHIP_PRICE));
return successCost + failureCost;
}
public static void main(String[] args) {
new GE32Tool().calculate();
}
}
(For reference with respect to variable names: The 29 DR item is called a "Le Noir" Armor)
When testing this application, you can use prices in the range of 300,000 to 1,000,000, that is the regular market value in the game.