Elden Ring is a hit game that has some interesting theorycrafting behind it.
There are hundreds of armor pieces, weapons, and spells. Finding optimal combinations of them based on player & item stats is an interesting practical problem.
I've always wanted to learn how to use Constraint Solvers and it seems a good usecase exists!
Goal:
Here is the repo:
My attempt so far:
@PlanningEntity
class for a combination of armor@PlanningSolution
class (not sure if this is correct)I managed to solve it after advice below
The trick was to change to use PlanningEntityProperty
:
@PlanningSolution
public class ArmorSetComboPlanningSolution {
public List<ArmorPiece> armorPieces;
public Map<Integer, List<ArmorPiece>> armorByType;
@ValueRangeProvider(id = "headRange")
@ProblemFactCollectionProperty
public List<ArmorPiece> headList;
@ValueRangeProvider(id = "chestRange")
@ProblemFactCollectionProperty
public List<ArmorPiece> chestList;
@ValueRangeProvider(id = "armsRange")
@ProblemFactCollectionProperty
public List<ArmorPiece> armsList;
@ValueRangeProvider(id = "legsRange")
@ProblemFactCollectionProperty
public List<ArmorPiece> legsList;
@PlanningEntityProperty
public ArmorSet armorSet;
@PlanningScore(bendableHardLevelsSize = 1, bendableSoftLevelsSize = 5)
BendableLongScore score;
ArmorSetComboPlanningSolution() {
}
ArmorSetComboPlanningSolution(List<ArmorPiece> armorPieces) {
this.armorPieces = armorPieces;
this.armorByType = armorPieces.stream().collect(groupingBy(ArmorPiece::armorCategoryID));
this.headList = armorByType.get(0);
this.chestList = armorByType.get(1);
this.armsList = armorByType.get(2);
this.legsList = armorByType.get(3);
// Need to initialize a starting value
this.armorSet = new ArmorSet(0L, this.headList.get(0), this.chestList.get(0), this.armsList.get(0), this.legsList.get(0));
}
}
Then the scorer:
public class ArmorSetEasyOptimizer implements EasyScoreCalculator<ArmorSetComboPlanningSolution, BendableLongScore> {
private final int TARGE_POISE = 61;
private final double MAX_WEIGHT = 60.64;
public ArmorSetEasyOptimizer() {
}
@Override
public BendableLongScore calculateScore(ArmorSetComboPlanningSolution solution) {
long hardScore = 0L;
ArmorSet armorSet = solution.armorSet;
if (armorSet.getTotalPoise() < TARGE_POISE) {
hardScore--;
}
if (armorSet.getTotalWeight() > MAX_WEIGHT) {
hardScore--;
}
long poiseRatio = (long) (armorSet.getTotalPoise() / (double) armorSet.getTotalWeight() * 100);
long physicalDefenseScaled = (long) (armorSet.getTotalPhysicalDefense() * 100);
long physicalDefenseToWeightRatio = (long) (physicalDefenseScaled / armorSet.getTotalWeight());
long magicDefenseScaled = (long) (armorSet.getTotalMagicDefense() * 100);
long magicDefenseToWeightRatio = (long) (magicDefenseScaled / armorSet.getTotalWeight());
return BendableLongScore.of(
new long[]{
hardScore
},
new long[]{
poiseRatio,
physicalDefenseScaled,
physicalDefenseToWeightRatio,
magicDefenseScaled,
magicDefenseToWeightRatio
}
);
}
}
19:02:12.707 [main] INFO org.optaplanner.core.impl.localsearch.DefaultLocalSearchPhase - Local Search phase (1) ended: time spent (10000), best score ([0]hard/[179/3540/97/2750/75]soft), score calculation speed (987500/sec), step total (4046).
19:02:12.709 [main] INFO org.optaplanner.core.impl.solver.DefaultSolver - Solving ended: time spent (10000), best score ([0]hard/[179/3540/97/2750/75]soft), score calculation speed (985624/sec), phase total (2), environment mode (REPRODUCIBLE), move thread count (NONE).
[0]hard/[179/3540/97/2750/75]soft
ArmorSet (Weight: 36.3, Poise: 65, Physical: 35.4, Phys/Weight: 0.97, Magic: 27.5, Magic/Weight: 0.75 ) [
head: Radahn Soldier Helm (Weight: 4.0, Poise: 5),
chest: Veteran's Armor (Weight: 18.9, Poise: 37),
arms: Godskin Noble Bracelets (Weight: 1.7, Poise: 1),
legs: Veteran's Greaves (Weight: 11.7, Poise: 22)
]
This is a bit of a strange problem, because you only need one planning entity instance. There only ever needs to be one ArmorSet
object - and the solver will be assigning different armor pieces as it comes closer and closer to an optimal combination.
Therefore your easy score calculator doesn't ever need to do any looping. It simply takes the single ArmorSet
's weight and poise and creates a score out of it.
However, even though I think that this use case may be useful as a learning path towards constraint solvers, some sort of a brute force algorithm could work as well - your data set isn't too large. More importantly, with an exhaustive algorithm such as brute force, you're eventually guaranteed to reach the optimal solution.
(That said, if you want to enhance the problem with matching these armor sets to particular character traits, then it perhaps may be complex enough for brute force to become inadequate.)
On a personal note, I attempted Elden Ring and found it too hardcore for me. :-) I prefer games that guide you a bit more.