Skip to content

Commit

Permalink
Merge pull request #657 from wowsims/auto_reforge
Browse files Browse the repository at this point in the history
Auto-Reforge Implementation
  • Loading branch information
NerdEgghead authored Jun 11, 2024
2 parents 126e8e3 + a9cd280 commit 9423c94
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 55 deletions.
16 changes: 15 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"pako": "^2.0.4",
"tippy.js": "^6.3.7",
"tsx-vanilla": "^1.0.0",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"yalps": "^0.5.6"
},
"devDependencies": {
"@protobuf-ts/plugin": "2.9.1",
Expand Down
5 changes: 1 addition & 4 deletions ui/core/components/individual_sim_ui/reforge_summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,7 @@ export class ReforgeSummary extends Component {
<button
className="btn btn-sm btn-reset summary-table-reset-button"
onclick={() => {
gear.getItemSlots().forEach(itemSlot => {
const item = gear.getEquippedItem(itemSlot);
if (item) gear = gear.withEquippedItem(itemSlot, item.withItem(item.item), this.player.canDualWield2H());
});
gear = gear.withoutReforges(this.player.canDualWield2H());
this.player.setGear(TypedEvent.nextEventID(), gear);
}}>
Reset reforges
Expand Down
221 changes: 221 additions & 0 deletions ui/core/components/suggest_reforges_action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { solve } from "yalps"
import { lessEq, equalTo, greaterEq, inRange } from "yalps"
import { Model, Constraint, Coefficients, OptimizationDirection, Options, Solution } from "yalps"
import { Player } from '../player.js';
import { Sim } from '../sim.js';
import { IndividualSimUI } from '../individual_sim_ui.js';
import { Stats } from '../proto_utils/stats.js';
import { TypedEvent } from '../typed_event.js';
import { Gear } from '../proto_utils/gear.js';
import { ItemSlot, Stat } from '../proto/common.js';

interface StatWeightsConfig {
statCaps: Stats;
preCapEPs: Stats;
}

type YalpsCoefficients = Map<string, number>;
type YalpsVariables = Map<string, YalpsCoefficients>;
type YalpsConstraints = Map<string, Constraint>;

export const sleep = async (waitTime: number) =>
new Promise(resolve =>
setTimeout(resolve, waitTime));

export class ReforgeOptimizer {
protected readonly player: Player<any>;
protected readonly sim: Sim;
protected readonly statCaps: Stats;
protected readonly preCapEPs: Stats;

constructor(simUI: IndividualSimUI<any>, config: StatWeightsConfig) {
this.player = simUI.player;
this.sim = simUI.sim;
this.statCaps = config.statCaps;
this.preCapEPs = config.preCapEPs;

simUI.addAction('Suggest Reforges', 'suggest-reforges-action', async () => {
this.optimizeReforges();
});
}

async optimizeReforges() {
console.log("Starting Reforge optimization...");

// First, clear all existing Reforges
console.log("Clearing existing Reforges...");
const baseGear = this.player.getGear().withoutReforges(this.player.canDualWield2H());
const baseStats = await this.updateGear(baseGear);

// Compute effective stat caps for just the Reforge contribution
const reforgeCaps = baseStats.computeStatCapsDelta(this.statCaps);
console.log("Stat caps for Reforge contribution:");
console.log(reforgeCaps);

// Set up YALPS model
const variables = this.buildYalpsVariables(baseGear);
const constraints = this.buildYalpsConstraints(baseGear);

// Solve in multiple passes to enforce caps
await this.solveModel(baseGear, reforgeCaps, variables, constraints);
}

async updateGear(gear: Gear): Promise<Stats> {
this.player.setGear(TypedEvent.nextEventID(), gear);
await this.sim.updateCharacterStats(TypedEvent.nextEventID());
return Stats.fromProto(this.player.getCurrentStats().finalStats);
}

buildYalpsVariables(gear: Gear): YalpsVariables {
const variables = new Map<string, YalpsCoefficients>();

for (const slot of gear.getItemSlots()) {
const item = gear.getEquippedItem(slot);

if (!item) {
continue;
}

for (const reforgeData of this.player.getAvailableReforgings(item)) {
const variableKey = `${slot}_${reforgeData.id}`;
const coefficients = new Map<string, number>();
coefficients.set(ItemSlot[slot], 1);

for (const fromStat of reforgeData.fromStat) {
coefficients.set(Stat[fromStat], reforgeData.fromAmount);
}

for (const toStat of reforgeData.toStat) {
coefficients.set(Stat[toStat], reforgeData.toAmount);
}

variables.set(variableKey, coefficients);
}
}

return variables;
}

buildYalpsConstraints(gear: Gear): YalpsConstraints {
const constraints = new Map<string, Constraint>();

for (const slot of gear.getItemSlots()) {
constraints.set(ItemSlot[slot], lessEq(1));
}

return constraints;
}

async solveModel(gear: Gear, reforgeCaps: Stats, variables: YalpsVariables, constraints: YalpsConstraints) {
// Calculate EP scores for each Reforge option
const updatedVariables = this.updateReforgeScores(variables, constraints);
console.log("Optimization variables and constraints for this iteration:");
console.log(updatedVariables);
console.log(constraints);

// Set up and solve YALPS model
const model: Model = {
direction: "maximize",
objective: "score",
constraints: constraints,
variables: updatedVariables,
binaries: true
};
const solution = solve(model);
console.log("LP solution for this iteration:");
console.log(solution);

// Apply the current solution
await this.applyLPSolution(gear, solution);

// Check if any unconstrained stats exceeded their specified cap.
// If so, add these stats to the constraint list and re-run the solver.
// If no unconstrained caps were exceeded, then we're done.
const [anyCapsExceeded, updatedConstraints] = this.checkCaps(solution, reforgeCaps, updatedVariables, constraints);

if (!anyCapsExceeded) {
console.log("Reforge optimization has converged!");
} else {
console.log("One or more stat caps were exceeded, starting constrained iteration...");
await sleep(100);
await this.solveModel(gear, reforgeCaps, updatedVariables, updatedConstraints);
}
}

updateReforgeScores(variables: YalpsVariables, constraints: YalpsConstraints): YalpsVariables {
const updatedVariables = new Map<string, YalpsCoefficients>();

for (const [variableKey, coefficients] of variables.entries()) {
let score = 0;
const updatedCoefficients = new Map<string, number>();

for (const [coefficientKey, value] of coefficients.entries()) {
updatedCoefficients.set(coefficientKey, value);

// Determine whether the key corresponds to a stat change.
// If so, check whether the stat has already been constrained to be capped in a previous iteration.
// Apply stored EP only for unconstrained stats.
if (coefficientKey.includes('Stat') && !constraints.has(coefficientKey)) {
const statKey = (Stat as any)[coefficientKey] as Stat;
score += this.preCapEPs.getStat(statKey) * value;
}
}

updatedCoefficients.set("score", score);
updatedVariables.set(variableKey, updatedCoefficients);
}

return updatedVariables;
}

async applyLPSolution(gear: Gear, solution: Solution) {
let updatedGear = gear.withoutReforges(this.player.canDualWield2H());

for (const [variableKey, _coefficient] of solution.variables) {
const splitKey = variableKey.split("_");
const slot = parseInt(splitKey[0]) as ItemSlot;
const reforgeId = parseInt(splitKey[1]);
const equippedItem = gear.getEquippedItem(slot);

if (equippedItem) {
updatedGear = updatedGear.withEquippedItem(slot, equippedItem.withReforge(this.sim.db.getReforgeById(reforgeId)!), this.player.canDualWield2H());
}
}

await this.updateGear(updatedGear);
}

checkCaps(solution: Solution, reforgeCaps: Stats, variables: YalpsVariables, constraints: YalpsConstraints): [boolean, YalpsConstraints] {
// First add up the total stat changes from the solution
let reforgeStatContribution = new Stats();

for (const [variableKey, _coefficient] of solution.variables) {
for (const [coefficientKey, value] of variables.get(variableKey)!.entries()) {
if (coefficientKey.includes('Stat')) {
const statKey = (Stat as any)[coefficientKey] as Stat;
reforgeStatContribution = reforgeStatContribution.addStat(statKey, value);
}
}
}

console.log("Total stat contribution from Reforging:");
console.log(reforgeStatContribution);

// Then check whether any unconstrained stats exceed their cap
let anyCapsExceeded = false;
const updatedConstraints = new Map<string, Constraint>(constraints);

for (const [statKey, value] of reforgeStatContribution.asArray().entries()) {
const cap = reforgeCaps.getStat(statKey);
const statName = Stat[statKey];

if ((cap != 0) && (value > cap) && !constraints.has(statName)) {
updatedConstraints.set(statName, greaterEq(cap));
anyCapsExceeded = true;
console.log("Cap exceeded for: %s", statName);
}
}

return [anyCapsExceeded, updatedConstraints];
}
}
14 changes: 14 additions & 0 deletions ui/core/proto_utils/gear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,20 @@ export class Gear extends BaseGear {
return curGear;
}

withoutReforges(canDualWield2H: boolean): Gear {
let curGear: Gear = this;

for (const slot of this.getItemSlots()) {
const item = this.getEquippedItem(slot);

if (item) {
curGear = curGear.withEquippedItem(slot, item.withItem(item.item).withRandomSuffix(item._randomSuffix), canDualWield2H);
}
}

return curGear;
}

// Removes bonus gems from blacksmith profession bonus.
withoutBlacksmithSockets(): Gear {
let curGear: Gear = this;
Expand Down
10 changes: 10 additions & 0 deletions ui/core/proto_utils/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,16 @@ export class Stats {
return true;
}

computeStatCapsDelta(statCaps: Stats): Stats {
return new Stats(this.stats.map((value, stat) => {
if (statCaps.stats[stat] > 0) {
return statCaps.stats[stat] - value;
}

return 0;
}));
}

asArray(): Array<number> {
return this.stats.slice();
}
Expand Down
45 changes: 10 additions & 35 deletions ui/druid/feral/sim.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as BuffDebuffInputs from '../../core/components/inputs/buffs_debuffs.js';
import * as OtherInputs from '../../core/components/other_inputs.js';
import { PhysicalDPSGemOptimizer } from '../../core/components/suggest_gems_action.js';
import { ReforgeOptimizer } from '../../core/components/suggest_reforges_action.js';
import { IndividualSimUI, registerSpecConfig } from '../../core/individual_sim_ui.js';
import { Player } from '../../core/player.js';
import { PlayerClasses } from '../../core/player_classes';
Expand All @@ -12,6 +12,7 @@ import { Gear } from '../../core/proto_utils/gear.js';
import { Stats } from '../../core/proto_utils/stats.js';
import * as FeralInputs from './inputs.js';
import * as Presets from './presets.js';
import * as Mechanics from '../../core/constants/mechanics.js';

const SPEC_CONFIG = registerSpecConfig(Spec.SpecFeralDruid, {
cssClass: 'feral-druid-sim-ui',
Expand Down Expand Up @@ -179,39 +180,13 @@ export class FeralDruidSimUI extends IndividualSimUI<Spec.SpecFeralDruid> {
constructor(parentElem: HTMLElement, player: Player<Spec.SpecFeralDruid>) {
super(parentElem, player, SPEC_CONFIG);

//const _gemOptimizer = new FeralGemOptimizer(this);
}
}

class FeralGemOptimizer extends PhysicalDPSGemOptimizer {
constructor(simUI: IndividualSimUI<Spec.SpecFeralDruid>) {
super(simUI, true, true, true, true);
}

calcCritCap(gear: Gear): Stats {
const baseCritCapPercentage = 77.8; // includes 3% Crit debuff
let agiProcs = 0;

if (gear.hasRelic(47668)) {
agiProcs += 200;
}

if (gear.hasRelic(50456)) {
agiProcs += 44 * 5;
}

if (gear.hasTrinket(47131) || gear.hasTrinket(47464)) {
agiProcs += 510;
}

if (gear.hasTrinket(47115) || gear.hasTrinket(47303)) {
agiProcs += 450;
}

if (gear.hasTrinket(44253) || gear.hasTrinket(42987)) {
agiProcs += 300;
}

return new Stats().withStat(Stat.StatMeleeCrit, (baseCritCapPercentage - (agiProcs * 1.1 * 1.06 * 1.02) / 83.33) * 45.91);
// Auto-Reforge configuration
const hitCap = new Stats().withStat(Stat.StatMeleeHit, 8 * Mechanics.MELEE_HIT_RATING_PER_HIT_CHANCE);
const expCap = new Stats().withStat(Stat.StatExpertise, 6.5 * 4 * Mechanics.EXPERTISE_PER_QUARTER_PERCENT_REDUCTION);
const statWeightsConfig = {
statCaps: hitCap.add(expCap),
preCapEPs: this.individualConfig.defaults.epWeights,
};
const reforgeOptimizer = new ReforgeOptimizer(this, statWeightsConfig);
}
}
Loading

0 comments on commit 9423c94

Please sign in to comment.