diff --git a/src/main/java/com/wynncraft/AlgorithmRegistry.java b/src/main/java/com/wynncraft/AlgorithmRegistry.java index 19e93b2..52c54b6 100644 --- a/src/main/java/com/wynncraft/AlgorithmRegistry.java +++ b/src/main/java/com/wynncraft/AlgorithmRegistry.java @@ -1,26 +1,7 @@ package com.wynncraft; -import com.wynncraft.algorithms.NegativeOrderAlgorithm; -import com.wynncraft.algorithms.CapyTopoAlgorithm; -import com.wynncraft.algorithms.CascadeBoundChecker; -import com.wynncraft.algorithms.GreedyAlgorithm; -import com.wynncraft.algorithms.MyFirstAlgorithm; -import com.wynncraft.algorithms.MySecondAlgorithm; -import com.wynncraft.algorithms.OurSecondAlgorithm; -import com.wynncraft.algorithms.PrunedMaskAlgorithm; -import com.wynncraft.algorithms.PrunedMaskV2Algorithm; -import com.wynncraft.algorithms.SCCGraphAlgorithm; -import com.wynncraft.algorithms.TheCuteCatAlgo; -import com.wynncraft.algorithms.TheFourthAlgorithm; -import com.wynncraft.algorithms.TheThirdAlgorithm; -import com.wynncraft.algorithms.SubtractiveBnBAlgorithm; -import com.wynncraft.algorithms.PrunedMaskAlgorithm; -import com.wynncraft.algorithms.PrunedMaskV2Algorithm; -import com.wynncraft.algorithms.StarvingGoblinAlgorithm; -import com.wynncraft.algorithms.StarvingPlayer; -import com.wynncraft.algorithms.WynnFrumaAlgorithm; -import com.wynncraft.algorithms.WynnSolverAlgorithm; +import com.wynncraft.algorithms.*; import com.wynncraft.core.WynnPlayer; import com.wynncraft.core.interfaces.IAlgorithm; import com.wynncraft.core.interfaces.IPlayerBuilder; @@ -56,6 +37,8 @@ public class AlgorithmRegistry { register(new PrunedMaskAlgorithm(), WynnPlayer.Builder::new); register(new PrunedMaskV2Algorithm(), WynnPlayer.Builder::new); register(new StarvingGoblinAlgorithm(), StarvingPlayer.Builder::new); + register(new FredAlgorithm(), WynnPlayer.Builder::new); + register(new FredV2Algorithm(), WynnPlayer.Builder::new); } /** diff --git a/src/main/java/com/wynncraft/algorithms/FredAlgorithm.java b/src/main/java/com/wynncraft/algorithms/FredAlgorithm.java new file mode 100644 index 0000000..2f8d8ea --- /dev/null +++ b/src/main/java/com/wynncraft/algorithms/FredAlgorithm.java @@ -0,0 +1,203 @@ +package com.wynncraft.algorithms; + +import com.wynncraft.core.WynnPlayer; +import com.wynncraft.core.fred.Utils.IntStack; +import com.wynncraft.core.fred.Utils.Vec5; +import com.wynncraft.core.interfaces.IAlgorithm; +import com.wynncraft.core.interfaces.IEquipment; +import com.wynncraft.core.interfaces.Information; +import com.wynncraft.enums.SkillPoint; + +import java.util.Arrays; +import java.util.List; + +/** + * Relatively simple algorithm that first assigns items that are guaranteed to be usable + * greedily and then performs a dfs for the remaining items. It uses a Vec5 class for + * skill points to allow the JIT to inline most operations. + */ +@Information(name = "Fred Algo", version = 1, authors = "Frederik") +public class FredAlgorithm implements IAlgorithm { + public FredAlgorithm() {} + + // Get the bonus SP of a specified combo. + public static void getComboSP(int combo, Vec5 baseSP, Vec5[] itemBonuses, Vec5 out) { + out.setFrom(baseSP); + while (combo != 0) { + int i = Integer.numberOfTrailingZeros(combo); + out.add(itemBonuses[i]); + combo &= combo - 1; + } + } + + private static boolean isZero(int[] arr) { + return arr[0] == 0 && arr[1] == 0 && arr[2] == 0 && arr[3] == 0 && arr[4] == 0; + } + + public Result run(WynnPlayer player) { + List items = player.equipment(); + final int N_ITEMS = items.size(); + boolean[] result = new boolean[N_ITEMS]; + + // Preprocess items + Vec5 assignedSP = new Vec5( + player.allocated(SkillPoint.STRENGTH), + player.allocated(SkillPoint.DEXTERITY), + player.allocated(SkillPoint.INTELLIGENCE), + player.allocated(SkillPoint.DEFENCE), + player.allocated(SkillPoint.AGILITY) + ); + Vec5 baseSP = new Vec5(assignedSP); + Vec5 minSP = new Vec5(assignedSP); + + Vec5[] itemReqs = new Vec5[N_ITEMS]; + Vec5[] itemBonuses = new Vec5[N_ITEMS]; + boolean[] negItems = new boolean[N_ITEMS]; + + for (int i = 0; i < N_ITEMS; i++) { + int[] req = items.get(i).requirements(); + int[] bonus = items.get(i).bonuses(); + + if (isZero(bonus) && isZero(req)) { + result[i] = true; + } else { + itemReqs[i] = new Vec5(req); + itemBonuses[i] = new Vec5(bonus); + if (itemBonuses[i].hasNeg()) { + negItems[i] = true; + minSP.addNegatives(itemBonuses[i]); + } + } + } + + // Greedy add all items that are guaranteed to be equippable + boolean added = true; + while (added) { + added = false; + for (int i = 0; i < N_ITEMS; i++) { + if (result[i] || negItems[i]) + continue; + if (Vec5.meetsReqs(minSP, itemReqs[i])) { + baseSP.add(itemBonuses[i]); + minSP.add(itemBonuses[i]); + added = true; + result[i] = true; + } + } + } + + // Clean up the item arrays for better efficiency + int itemCtr = 0; + int[] itemIdxs = new int[N_ITEMS]; + for (int i = 0; i < N_ITEMS; i++) { + if (result[i]) + continue; + + itemIdxs[itemCtr] = i; + itemReqs[itemCtr] = itemReqs[i]; + itemBonuses[itemCtr] = itemBonuses[i]; + negItems[itemCtr] = negItems[i]; + itemCtr++; + } + + final int N_UNDETERMINED = itemCtr; + final int N_COMBOS = 1 << N_UNDETERMINED; + + /* + * Each combination of items is represented using an integer as a bitmask, where each bit corresponds to one item. + * For example for N_UNDETERMINED = 4 the combo number 15 (binary 1111) would be the combo with all four items. + * This part runs a dfs to find the best valid combination. + */ + int bestCombo = 0; + int bestSPTotal = baseSP.total(); + int bestItemCount = 0; + + boolean[] checkedCombos = new boolean[N_COMBOS]; + checkedCombos[0] = true; + + IntStack stack = new IntStack(32); + stack.push(0); + + Vec5 comboSP = new Vec5(); + Vec5 newComboSP = new Vec5(); + Vec5 newComboSP2 = new Vec5(); + + while (!stack.isEmpty()) { + int combo = stack.pop(); + getComboSP(combo, baseSP, itemBonuses, comboSP); + + if (combo == N_COMBOS - 1) + break; // The whole build is valid + + // Add 1 item + for (int itemIdx = 0; itemIdx < N_UNDETERMINED; itemIdx++) { + int itemBit = 1 << itemIdx; + if ((combo & itemBit) != 0) + continue; + int newCombo = combo | itemBit; + + if (checkedCombos[newCombo]) + continue; // Already checked combo + checkedCombos[newCombo] = true; + + if (!Vec5.meetsReqs(comboSP, itemReqs[itemIdx])) + continue; + + newComboSP.setFrom(comboSP); + newComboSP.add(itemBonuses[itemIdx]); + + // Recheck all items if the added item has negative bonuses + if (negItems[itemIdx]) { + boolean valid = true; + for (int itemIdx2 = 0; itemIdx2 < N_UNDETERMINED; itemIdx2++) { + int itemBit2 = 1 << itemIdx2; + if ((combo & itemBit2) == 0) + continue; + + newComboSP2.setFrom(newComboSP); + newComboSP2.sub(itemBonuses[itemIdx2]); + if (!Vec5.meetsReqs(newComboSP2, itemReqs[itemIdx2])) { + valid = false; + break; + } + } + if (!valid) + continue; + } + + int spTot = newComboSP.total(); + int itemCount = Integer.bitCount(newCombo); + if (itemCount > bestItemCount || ((itemCount == bestItemCount && spTot > bestSPTotal))) { + bestCombo = newCombo; + bestSPTotal = spTot; + bestItemCount = itemCount; + } + stack.push(newCombo); + } + } + + // Process result + for (int itemIdx = 0; itemIdx < N_UNDETERMINED; itemIdx++) { + if ((bestCombo & (1 << itemIdx)) != 0) { + result[itemIdxs[itemIdx]] = true; + baseSP.add(itemBonuses[itemIdx]); + } + } + baseSP.sub(assignedSP); + player.modify(baseSP.asArray(), true); + + int totalValid = bestItemCount + (N_ITEMS - N_UNDETERMINED); + IEquipment[] validItems = new IEquipment[totalValid]; + IEquipment[] invalidItems = new IEquipment[N_ITEMS - totalValid]; + int validCtr = 0; + int invalidCtr = 0; + for (int i = 0; i < N_ITEMS; i++) { + if (result[i]) + validItems[validCtr++] = items.get(i); + else + invalidItems[invalidCtr++] = items.get(i); + } + return new Result(Arrays.asList(validItems), Arrays.asList(invalidItems)); + } +} + diff --git a/src/main/java/com/wynncraft/algorithms/FredV2Algorithm.java b/src/main/java/com/wynncraft/algorithms/FredV2Algorithm.java new file mode 100644 index 0000000..4edc792 --- /dev/null +++ b/src/main/java/com/wynncraft/algorithms/FredV2Algorithm.java @@ -0,0 +1,217 @@ +package com.wynncraft.algorithms; + +import com.wynncraft.core.WynnPlayer; +import com.wynncraft.core.fred.Utils.IntStack; +import com.wynncraft.core.fred.Utils.Vec5; +import com.wynncraft.core.interfaces.IAlgorithm; +import com.wynncraft.core.interfaces.IEquipment; +import com.wynncraft.core.interfaces.Information; +import com.wynncraft.enums.SkillPoint; + +import java.util.Arrays; +import java.util.List; + +/** + * V2 allocates the item arrays in advance. It makes debugging a bit harder though. + */ +@Information(name = "Fred Algo", version = 2, authors = "Frederik") +public class FredV2Algorithm implements IAlgorithm { + private static final int N_ITEMS_MAX = 32; + + private final Vec5[] itemReqs; + private final Vec5[] itemBonuses; + private final boolean[] negItems; + private final boolean[] result; + private final int[] itemIdxs; + + public FredV2Algorithm() { + itemReqs = new Vec5[N_ITEMS_MAX]; + itemBonuses = new Vec5[N_ITEMS_MAX]; + negItems = new boolean[N_ITEMS_MAX]; + + for (int i = 0; i < N_ITEMS_MAX; i++) { + itemReqs[i] = new Vec5(); + itemBonuses[i] = new Vec5(); + } + + result = new boolean[N_ITEMS_MAX]; + itemIdxs = new int[N_ITEMS_MAX]; + } + + // Get the bonus SP of a specified combo. + public static void getComboSP(int combo, Vec5 baseSP, Vec5[] itemBonuses, Vec5 out) { + out.setFrom(baseSP); + while (combo != 0) { + int i = Integer.numberOfTrailingZeros(combo); + out.add(itemBonuses[i]); + combo &= combo - 1; + } + } + + private static boolean isZero(int[] arr) { + return arr[0] == 0 && arr[1] == 0 && arr[2] == 0 && arr[3] == 0 && arr[4] == 0; + } + + public Result run(WynnPlayer player) { + Arrays.fill(result, false); + Arrays.fill(negItems, false); + + // Preprocess items + List items = player.equipment(); + final int N_ITEMS = items.size(); + Vec5 assignedSP = new Vec5( + player.allocated(SkillPoint.STRENGTH), + player.allocated(SkillPoint.DEXTERITY), + player.allocated(SkillPoint.INTELLIGENCE), + player.allocated(SkillPoint.DEFENCE), + player.allocated(SkillPoint.AGILITY) + ); + Vec5 baseSP = new Vec5(assignedSP); + Vec5 minSP = new Vec5(assignedSP); + + for (int i = 0; i < N_ITEMS; i++) { + int[] req = items.get(i).requirements(); + int[] bonus = items.get(i).bonuses(); + + if (isZero(bonus) && isZero(req)) { + result[i] = true; // Filter out tomes and similar items + } else { + itemReqs[i].setFrom(req); + itemBonuses[i].setFrom(bonus); + if (itemBonuses[i].hasNeg()) { + negItems[i] = true; + minSP.addNegatives(itemBonuses[i]); + } + } + } + + // Greedy add all items that are guaranteed to be equippable + boolean added = true; + while (added) { + added = false; + for (int i = 0; i < N_ITEMS; i++) { + if (result[i] || negItems[i]) + continue; + if (Vec5.meetsReqs(minSP, itemReqs[i])) { + baseSP.add(itemBonuses[i]); + minSP.add(itemBonuses[i]); + added = true; + result[i] = true; + } + } + } + + // Clean up the item arrays for better efficiency + int itemCtr = 0; + for (int i = 0; i < N_ITEMS; i++) { + if (result[i]) + continue; + + itemIdxs[itemCtr] = i; + itemReqs[itemCtr].setFrom(itemReqs[i]); + itemBonuses[itemCtr].setFrom(itemBonuses[i]); + negItems[itemCtr] = negItems[i]; + itemCtr++; + } + + /* + * Each combination of items is represented using an integer as a bitmask, where each bit corresponds to one item. + * For example for N_UNDETERMINED = 4 the combo number 15 (binary 1111) would be the combo with all four items. + * This part runs a dfs to find the best valid combination. + */ + final int N_UNDETERMINED = itemCtr; + final int N_COMBOS = 1 << N_UNDETERMINED; + + int bestCombo = 0; + int bestSPTotal = baseSP.total(); + int bestItemCount = 0; + + boolean[] checkedCombos = new boolean[N_COMBOS]; + checkedCombos[0] = true; + + IntStack stack = new IntStack(32); + stack.push(0); + + Vec5 comboSP = new Vec5(); + Vec5 newComboSP = new Vec5(); + Vec5 newComboSP2 = new Vec5(); + + while (!stack.isEmpty()) { + int combo = stack.pop(); + getComboSP(combo, baseSP, itemBonuses, comboSP); + + if (combo == N_COMBOS - 1) + break; // The whole build is valid + + // Add 1 item + for (int itemIdx = 0; itemIdx < N_UNDETERMINED; itemIdx++) { + int itemBit = 1 << itemIdx; + if ((combo & itemBit) != 0) + continue; + int newCombo = combo | itemBit; + + if (checkedCombos[newCombo]) + continue; // Already checked combo + checkedCombos[newCombo] = true; + + if (!Vec5.meetsReqs(comboSP, itemReqs[itemIdx])) + continue; + + newComboSP.setFrom(comboSP); + newComboSP.add(itemBonuses[itemIdx]); + + // Recheck all items if the added item has negative bonuses + if (negItems[itemIdx]) { + boolean valid = true; + for (int itemIdx2 = 0; itemIdx2 < N_UNDETERMINED; itemIdx2++) { + int itemBit2 = 1 << itemIdx2; + if ((combo & itemBit2) == 0) + continue; + + newComboSP2.setFrom(newComboSP); + newComboSP2.sub(itemBonuses[itemIdx2]); + if (!Vec5.meetsReqs(newComboSP2, itemReqs[itemIdx2])) { + valid = false; + break; + } + } + if (!valid) + continue; + } + + int spTot = newComboSP.total(); + int itemCount = Integer.bitCount(newCombo); + if (itemCount > bestItemCount || ((itemCount == bestItemCount && spTot > bestSPTotal))) { + bestCombo = newCombo; + bestSPTotal = spTot; + bestItemCount = itemCount; + } + stack.push(newCombo); + } + } + + // Process result + for (int itemIdx = 0; itemIdx < N_UNDETERMINED; itemIdx++) { + if ((bestCombo & (1 << itemIdx)) != 0) { + result[itemIdxs[itemIdx]] = true; + baseSP.add(itemBonuses[itemIdx]); + } + } + baseSP.sub(assignedSP); + player.modify(baseSP.asArray(), true); + + int totalValid = bestItemCount + (N_ITEMS - N_UNDETERMINED); + IEquipment[] validItems = new IEquipment[totalValid]; + IEquipment[] invalidItems = new IEquipment[N_ITEMS - totalValid]; + int validCtr = 0; + int invalidCtr = 0; + for (int i = 0; i < N_ITEMS; i++) { + if (result[i]) + validItems[validCtr++] = items.get(i); + else + invalidItems[invalidCtr++] = items.get(i); + } + return new Result(Arrays.asList(validItems), Arrays.asList(invalidItems)); + } +} + diff --git a/src/main/java/com/wynncraft/core/fred/Utils.java b/src/main/java/com/wynncraft/core/fred/Utils.java new file mode 100644 index 0000000..7f32057 --- /dev/null +++ b/src/main/java/com/wynncraft/core/fred/Utils.java @@ -0,0 +1,146 @@ +package com.wynncraft.core.fred; + +public final class Utils { + public static final class Vec5 { + public int a, b, c, d, e; + + public Vec5(int[] x) { + if (x.length != 5) + throw new IllegalArgumentException("Input must have length 5."); + a = x[0]; + b = x[1]; + c = x[2]; + d = x[3]; + e = x[4]; + } + + public Vec5(Vec5 x) { + a = x.a; + b = x.b; + c = x.c; + d = x.d; + e = x.e; + } + + public Vec5(int a, int b, int c, int d, int e) { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.e = e; + } + + public Vec5() { + } + + public static boolean meetsReqs(Vec5 sp, Vec5 reqs) { + return (reqs.a == 0 || reqs.a <= sp.a) + && (reqs.b == 0 || reqs.b <= sp.b) + && (reqs.c == 0 || reqs.c <= sp.c) + && (reqs.d == 0 || reqs.d <= sp.d) + && (reqs.e == 0 || reqs.e <= sp.e); + } + + public void add(Vec5 y) { + a += y.a; + b += y.b; + c += y.c; + d += y.d; + e += y.e; + } + + public void addNegatives(Vec5 y) { + if (y.a < 0) + a += y.a; + if (y.b < 0) + b += y.b; + if (y.c < 0) + c += y.c; + if (y.d < 0) + d += y.d; + if (y.e < 0) + e += y.e; + } + + public void sub(Vec5 y) { + a -= y.a; + b -= y.b; + c -= y.c; + d -= y.d; + e -= y.e; + } + + public void setFrom(Vec5 y) { + a = y.a; + b = y.b; + c = y.c; + d = y.d; + e = y.e; + } + + public void setFrom(int[] y) { + if (y.length != 5) + throw new IllegalArgumentException("Input must have length 5."); + a = y[0]; + b = y[1]; + c = y[2]; + d = y[3]; + e = y[4]; + } + + public int total() { + return a + b + c + d + e; + } + + public boolean hasNeg() { + return a < 0 || b < 0 || c < 0 || d < 0 || e < 0; + } + + public boolean isZero() { + return a == 0 && b == 0 && c == 0 && d == 0 && e == 0; + } + + @Override + public String toString() { + return "(" + a + ", " + b + ", " + c + ", " + d + ", " + e + ")"; + } + + public int[] asArray() { + return new int[]{a, b, c, d, e}; + } + } + + public static final class IntStack { + private int[] stack; + private int top = 0; + + public IntStack(int init_capacity) { + stack = new int[init_capacity]; + } + + public void push(int x) { + if (top == stack.length) + expand(); + stack[top++] = x; + } + + public int pop() { + return stack[--top]; + } + + public boolean isEmpty() { + return top == 0; + } + + private void expand() { + int newCap = stack.length << 1; + int[] newStack = new int[newCap]; + System.arraycopy(stack, 0, newStack, 0, stack.length); + stack = newStack; + } + + public void clear() { + top = 0; + } + } +}